<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Troubleshooting &#8211; The Self-Host Journal</title>
	<atom:link href="https://selfhostjournal.eu/category/troubleshooting/feed/" rel="self" type="application/rss+xml" />
	<link>https://selfhostjournal.eu</link>
	<description>Real setups, real fixes</description>
	<lastBuildDate>Fri, 23 Oct 2026 19:18:45 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.3</generator>

<image>
	<url>https://selfhostjournal.eu/wp-content/uploads/2026/10/site-icon-terminal-150x150.png</url>
	<title>Troubleshooting &#8211; The Self-Host Journal</title>
	<link>https://selfhostjournal.eu</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Traefik GeoBlock locked out my own AI assistant: allowing a published IP range</title>
		<link>https://selfhostjournal.eu/traefik-geoblock-allowed-ip-range/</link>
		
		<dc:creator><![CDATA[selfhostjournal]]></dc:creator>
		<pubDate>Fri, 23 Oct 2026 19:18:45 +0000</pubDate>
				<category><![CDATA[Security and SSO]]></category>
		<category><![CDATA[Troubleshooting]]></category>
		<guid isPermaLink="false">https://selfhostjournal.eu/?p=165</guid>

					<description><![CDATA[After switching on GeoBlock, Claude's connector to Home Assistant stopped working while everything else was fine. The logs showed why, and one allowedIPAddresses entry fixed it.]]></description>
										<content:encoded><![CDATA[
<div class="wp-block-group dn-summary has-global-padding is-layout-constrained wp-block-group-is-layout-constrained">
<p class="dn-label wp-block-paragraph">In short</p>



<p class="wp-block-paragraph"><strong>Problem:</strong> After I switched on Traefik GeoBlock, Claude&#8217;s connector to Home Assistant stopped working, while Home Assistant itself was fine for everyone at home.</p>



<p class="wp-block-paragraph"><strong>Solution:</strong> The connector calls from Anthropic&#8217;s servers in the US, and GeoBlock only allowed my own countries. One <code>allowedIPAddresses</code> entry with the provider&#8217;s published IP range fixed it.</p>



<p class="wp-block-paragraph"><strong>Tested with:</strong> Traefik 3, GeoBlock plugin (PascalMinder) 0.3.8, Home Assistant with an MCP server for Claude</p>
</div>



<p class="wp-block-paragraph">In <a href="/traefik-hardening-geoblock-crowdsec-sso/">Hardening Traefik</a> I put GeoBlock in front of every service: only visitors from the countries my family lives in get through, everyone else gets a 403. It worked exactly as planned. A bit too well, as it turned out: it also locked out a visitor I wanted, my own AI assistant.</p>



<p class="wp-block-paragraph">This is a short story about a security measure that does its job, breaks something you need, and how to find out which one it was.</p>



<h2 class="wp-block-heading">1. The symptom</h2>



<p class="wp-block-paragraph">I use Claude with a connector to Home Assistant, so I can ask about sensors, automations and logs in plain language. Shortly after the hardening, the connector couldn&#8217;t reach Home Assistant anymore. Every attempt failed.</p>



<p class="wp-block-paragraph">At the same time, Home Assistant was perfectly fine: the dashboard loaded at home, the app on my phone worked, automations ran. Only this one client was affected.</p>



<h2 class="wp-block-heading">2. What I checked first (and why it misled me)</h2>



<p class="wp-block-paragraph">A connector that signs in with OAuth has plenty of parts that can break, so that&#8217;s where you look first:</p>



<ul class="wp-block-list">
<li><strong>Sign-in and tokens:</strong> an expired token or a broken OAuth setup would explain a failing client. But reconnecting doesn&#8217;t help when the requests are blocked before they arrive.</li>



<li><strong>Home Assistant logs:</strong> nothing. No failed login, no error from the MCP integration, not even a request.</li>



<li><strong>The URL:</strong> it opens fine in the browser.</li>
</ul>



<p class="wp-block-paragraph">The empty Home Assistant log was the real clue, and it&#8217;s easy to read it the wrong way. &#8220;No errors&#8221; sounded like &#8220;nothing wrong in Home Assistant&#8221;. It actually meant: the requests never arrived. Something in front of Home Assistant was answering them. And &#8220;it works in my browser&#8221; proved nothing, because my browser comes from an allowed country.</p>



<h2 class="wp-block-heading">3. The real cause: Traefik GeoBlock did its job</h2>



<p class="wp-block-paragraph">Claude&#8217;s connectors don&#8217;t run in my browser or on my phone. They call my server from Anthropic&#8217;s infrastructure, which sits in the US. For GeoBlock that&#8217;s just another request from a country that isn&#8217;t on the list.</p>



<p class="wp-block-paragraph">Two logs show it. GeoBlock writes every denied request to its own file. A line looks like this (example address):</p>



<pre class="wp-block-code"><code>INFO: GeoBlock: 2026/10/16 09:14:52 geoblock@docker: request denied [203.0.113.17] for country [US] due to: country is not allowed</code></pre>



<p class="wp-block-paragraph">And the Traefik access log shows the 403 with an empty backend (<code>"-"</code> after the router name). That dash means the request never reached the service (again an example line):</p>



<pre class="wp-block-code"><code>203.0.113.17 - - [16/Oct/2026:09:14:52 +0000] "POST /api/webhook/… HTTP/1.1" 403 0 "-" "-" 4711 "homeassistant@docker" "-" 1ms</code></pre>



<p class="wp-block-paragraph">The log is full of scanners from all over the world, so the connector&#8217;s requests don&#8217;t stand out by themselves. Compare the denied addresses with the range Anthropic publishes for its outgoing traffic, and they do.</p>



<div class="wp-block-group dn-tip has-global-padding is-layout-constrained wp-block-group-is-layout-constrained">
<p class="wp-block-paragraph"><strong>Tip:</strong> when one client fails and the app&#8217;s own log is empty, look at the reverse proxy first. A 403 with no backend in the access log is the typical sign of a middleware saying no.</p>
</div>



<h2 class="wp-block-heading">4. The fix: allow the published IP range</h2>



<p class="wp-block-paragraph">GeoBlock has an option for exactly this: <code>allowedIPAddresses</code>. Addresses in this list are let through no matter which country they come from. One more label on the Traefik container:</p>



<pre class="wp-block-code"><code>      # Claude connectors call from Anthropic's published outbound range
      # source: https://platform.claude.com/docs/en/api/ip-addresses
      - "traefik.http.middlewares.geoblock.plugin.geoblock.allowedIPAddresses[0]=160.79.104.0/21"</code></pre>



<p class="wp-block-paragraph">Then recreate the container so Traefik reads the new labels:</p>



<pre class="wp-block-code"><code>docker compose up -d traefik</code></pre>



<p class="wp-block-paragraph">The range above is the one Anthropic lists at the time of writing. Check <a href="https://platform.claude.com/docs/en/api/ip-addresses" target="_blank" rel="noreferrer noopener">Anthropic&#8217;s IP address page</a> for the current one before you copy it. The same approach works for any other service that has to reach you from abroad, as long as its provider publishes its IP ranges.</p>



<div class="wp-block-group dn-warning has-global-padding is-layout-constrained wp-block-group-is-layout-constrained">
<p class="wp-block-paragraph"><strong>Warning:</strong> only allow ranges the provider publishes officially. Never add an IP you found in your own log just because it looks right, and never open a whole country for one service. Also keep in mind what an exception means: this range is shared by all of the provider&#8217;s customers, not just you. GeoBlock lets them through, so the login of the service behind it still has to hold. In my case Home Assistant&#8217;s own sign-in and CrowdSec are still in place.</p>
</div>



<h2 class="wp-block-heading">5. How to verify</h2>



<ol class="wp-block-list">
<li>Check that Traefik picked up the label:<br><code>docker inspect traefik --format '{{ index .Config.Labels "traefik.http.middlewares.geoblock.plugin.geoblock.allowedIPAddresses[0]" }}'</code></li>



<li>Use the connector once (in my case: ask Claude for a sensor value).</li>



<li>Make sure the range no longer shows up as denied:<br><code>grep -E '\[160\.79\.1(0[4-9]|1[01])\.' traefik-logs/geoblock.log | tail</code><br>No new lines after the restart means GeoBlock lets the connector through.</li>
</ol>



<h2 class="wp-block-heading">What I learned</h2>



<ul class="wp-block-list">
<li>&#8220;Works in my browser&#8221; is no test for a geo-block. Ask where the failing client really calls from.</li>



<li>An empty application log is a finding, not a dead end: the request was stopped earlier.</li>



<li>Before switching on an allow list, write down every service that calls in from outside: AI connectors, webhooks, monitoring, push services. Each one needs a deliberate exception or a different path.</li>
</ul>



<p class="wp-block-paragraph">The full GeoBlock and CrowdSec setup is in <a href="/traefik-hardening-geoblock-crowdsec-sso/">Hardening Traefik: geo-blocking, CrowdSec and single sign-on</a>.</p>

]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Authentik: &#8220;The request is otherwise malformed&#8221; after creating a provider through the API</title>
		<link>https://selfhostjournal.eu/authentik-request-otherwise-malformed/</link>
		
		<dc:creator><![CDATA[selfhostjournal]]></dc:creator>
		<pubDate>Fri, 23 Oct 2026 19:07:29 +0000</pubDate>
				<category><![CDATA[Security and SSO]]></category>
		<category><![CDATA[Troubleshooting]]></category>
		<guid isPermaLink="false">https://selfhostjournal.eu/?p=160</guid>

					<description><![CDATA[Every login through a new Authentik provider failed with invalid_request. The cause was one missing field in the API call, and the browser error points the wrong way.]]></description>
										<content:encoded><![CDATA[
<div class="wp-block-group dn-summary has-global-padding is-layout-constrained wp-block-group-is-layout-constrained">
<p class="dn-label wp-block-paragraph">In short</p>



<p class="wp-block-paragraph"><strong>Problem:</strong> Every login through a new Authentik OAuth2/OpenID provider fails with <code>invalid_request</code>: &#8220;The request is otherwise malformed&#8221;.</p>



<p class="wp-block-paragraph"><strong>Solution:</strong> The provider was created through the API, and its <code>grant_types</code> list was empty. Set it to <code>authorization_code</code> and <code>refresh_token</code>, then test the authorize URL with curl.</p>



<p class="wp-block-paragraph"><strong>Tested with:</strong> Authentik 2026.8, WordPress with the OpenID Connect Generic plugin 3.11</p>
</div>



<p class="wp-block-paragraph">I add most apps to Authentik through the admin interface. For this blog I wanted to script it, so I created the OAuth2 provider with a single API call. The call succeeded, the application showed up, the WordPress plugin was configured with the right client ID, secret and endpoints. And then not a single login worked.</p>



<p class="wp-block-paragraph">The fix turned out to be one field. Finding it took longer than it should have, because the error message points in the wrong direction.</p>



<h2 class="wp-block-heading">1. The symptom: &#8220;request is otherwise malformed&#8221;</h2>



<p class="wp-block-paragraph">Clicking &#8220;Login with OpenID Connect&#8221; in WordPress sent me to Authentik, and Authentik sent me straight back with an error page instead of a login form:</p>



<pre class="wp-block-code"><code>invalid_request
The request is otherwise malformed</code></pre>



<p class="wp-block-paragraph">No username prompt, no password prompt. Authentik refused the request before the login flow even started.</p>



<h2 class="wp-block-heading">2. What I checked first (and why it misled me)</h2>



<p class="wp-block-paragraph">&#8220;Malformed request&#8221; sounds like the client sent something wrong. So I went through the usual suspects on the WordPress side:</p>



<ul class="wp-block-list">
<li><strong>Redirect URI:</strong> character by character against the provider. Authentik uses strict matching, so a missing slash or a different query string is enough to break it. It matched.</li>



<li><strong>Client ID:</strong> copied again, no stray spaces.</li>



<li><strong>Scopes:</strong> <code>openid email profile</code>, all three available in the provider.</li>



<li><strong>Endpoints:</strong> taken from the provider&#8217;s <code>.well-known/openid-configuration</code>.</li>
</ul>



<p class="wp-block-paragraph">Everything was correct. The request from WordPress was fine. The problem was on the Authentik side, and the browser error doesn&#8217;t tell you that.</p>



<h2 class="wp-block-heading">3. The real cause: an empty grant_types list</h2>



<p class="wp-block-paragraph">The Authentik server log had the more useful message. With Docker Compose:</p>



<pre class="wp-block-code"><code>docker compose logs server --since 10m | grep -i grant</code></pre>



<p class="wp-block-paragraph">It said &#8220;Invalid grant_type for provider&#8221;. Every OAuth2 provider in Authentik has a list of allowed grant types. When you create a provider in the admin interface, the form fills in sensible defaults. The API doesn&#8217;t: <code>grant_types</code> is an optional field, and if you leave it out, the provider is saved with an <strong>empty list</strong>. An empty list means no grant type is allowed, so not even the normal authorization code flow works.</p>



<p class="wp-block-paragraph">My API call looked roughly like this (shortened):</p>



<pre class="wp-block-code"><code>curl -X POST https://id.example.com/api/v3/providers/oauth2/ \
  -H "Authorization: Bearer $AUTHENTIK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Provider for My Blog",
    "client_type": "confidential",
    "authorization_flow": "&lt;flow uuid&gt;",
    "invalidation_flow": "&lt;flow uuid&gt;",
    "redirect_uris": [
      {"matching_mode": "strict",
       "url": "https://blog.example.com/wp-admin/admin-ajax.php?action=openid-connect-authorize"}
    ],
    "signing_key": "&lt;certificate uuid&gt;",
    "property_mappings": ["&lt;openid&gt;", "&lt;email&gt;", "&lt;profile&gt;"]
  }'</code></pre>



<p class="wp-block-paragraph">Every required field is there, so the API accepts it without a warning. The one field that decides whether anyone can log in is the one that&#8217;s missing.</p>



<h2 class="wp-block-heading">4. The fix</h2>



<p class="wp-block-paragraph">Set the grant types on the existing provider. A login flow for a website needs <code>authorization_code</code>, and <code>refresh_token</code> if the app renews its tokens:</p>



<pre class="wp-block-code"><code>curl -X PATCH https://id.example.com/api/v3/providers/oauth2/&lt;provider id&gt;/ \
  -H "Authorization: Bearer $AUTHENTIK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"grant_types": ["authorization_code", "refresh_token"]}'</code></pre>



<p class="wp-block-paragraph">You can also open the provider in the admin interface and save it there with both grant types ticked. For new providers, add the line straight to the create call:</p>



<pre class="wp-block-code"><code>    "grant_types": ["authorization_code", "refresh_token"],</code></pre>



<p class="wp-block-paragraph">While you&#8217;re at it, compare the token lifetimes too (<code>access_token_validity</code>, <code>refresh_token_threshold</code>) with a provider you created in the interface. API-created providers don&#8217;t always get the same values, and it&#8217;s easier to keep all providers alike.</p>



<div class="wp-block-group dn-warning has-global-padding is-layout-constrained wp-block-group-is-layout-constrained">
<p class="wp-block-paragraph"><strong>Warning:</strong> only allow the grant types the app really uses. <code>password</code> and <code>client_credentials</code> skip the login flow, and with it your MFA and policies. A website login doesn&#8217;t need them.</p>
</div>



<h2 class="wp-block-heading">5. How to verify (without a browser)</h2>



<p class="wp-block-paragraph">You don&#8217;t need to click through the login to test a provider. Call the authorize endpoint with curl and look at where Authentik sends you:</p>



<pre class="wp-block-code"><code>curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' \
  "https://id.example.com/application/o/authorize/?client_id=&lt;client id&gt;&amp;response_type=code&amp;scope=openid&amp;redirect_uri=https%3A%2F%2Fblog.example.com%2Fwp-admin%2Fadmin-ajax.php%3Faction%3Dopenid-connect-authorize"</code></pre>



<ul class="wp-block-list">
<li><strong>Working:</strong> a <code>302</code> to Authentik&#8217;s own login flow, for example <code>/if/flow/default-authentication-flow/…</code>.</li>



<li><strong>Broken:</strong> a <code>302</code> back to your app&#8217;s redirect URI with <code>error=…</code> in the URL. The error name tells you what Authentik didn&#8217;t like.</li>
</ul>



<div class="wp-block-group dn-tip has-global-padding is-layout-constrained wp-block-group-is-layout-constrained">
<p class="wp-block-paragraph"><strong>Tip:</strong> the redirect URI in the curl call must be URL-encoded and match the provider exactly, otherwise you test the wrong problem. I now run this check after every provider I create, whether through the API or the interface. It takes five seconds.</p>
</div>



<h2 class="wp-block-heading">What I learned</h2>



<ul class="wp-block-list">
<li>&#8220;Malformed request&#8221; in the browser doesn&#8217;t mean the client is wrong. Read the identity provider&#8217;s server log before you change anything in the app.</li>



<li>The admin interface and the API don&#8217;t create the same object. The interface fills in defaults, the API stores exactly what you send.</li>



<li>A curl test of the authorize URL checks a provider in seconds, without logging in.</li>
</ul>



<p class="wp-block-paragraph">How the blog login is set up end to end is in <a href="/wordpress-sso-authentik/">Single sign-on for WordPress with Authentik</a>. All provider fields are described in the <a href="https://docs.goauthentik.io/" target="_blank" rel="noreferrer noopener">official Authentik documentation</a>.</p>

]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
