Keycloak Behind a Reverse Proxy: Fix 403 and Hostname Errors
Why Keycloak returns 403 behind a proxy, rejects redirect URIs, and issues tokens with the wrong issuer, and the exact options that resolve each case.
Almost every first Keycloak deployment fails the same way. The container starts, the logs look clean, and then the admin console will not load, the login page redirects into a loop, or an application rejects a perfectly valid token because the issuer is wrong. The cause is nearly always the same underlying problem: Keycloak does not know what URL the outside world uses to reach it, or it does not trust the proxy telling it.
This guide maps the symptoms to their documented causes and to the exact configuration options that resolve them.
Why This Class of Error Exists
Keycloak issues absolute URLs into things that outlive the request. A token’s iss claim, the endpoints published at the OpenID discovery document, the redirect target after a login, and every link in the admin console are all built from Keycloak’s idea of its own address.
Behind a reverse proxy, the request that reaches Keycloak has been rewritten. The scheme may be plain HTTP even though the client used HTTPS. The Host header may be an internal service name. The port is often not the public one. If Keycloak builds its URLs from that rewritten request, every URL it emits is wrong in a way that only shows up one hop later.
There are exactly two ways to solve it: tell Keycloak its public address explicitly, or tell it to trust the forwarding headers the proxy adds. The documentation supports both, and most failures come from doing neither or doing half of each.
Symptom: 403 Forbidden on Requests Through the Proxy
This one has an unusually direct answer in the documentation: “If you are using a reverse proxy for anything other than TLS passthrough and do not set the proxy-headers option, then by default you will see 403 Forbidden responses to requests via the proxy that perform origin checking.”
The qualifier at the end explains why the failure looks intermittent. Requests that carry an Origin header, which is what the login form post and the token endpoint calls from a browser do, get checked against Keycloak’s idea of its own address and rejected. A plain GET of the discovery document from curl sails through. So a deployment can look half-working: machine-to-machine calls succeed while every browser login returns 403.
The fix is to set proxy-headers to one of its two documented values:
forwarded— the standardForwardedheader defined by RFC 7239.xforwarded— the non-standard but widespreadX-Forwarded-*family.
Pick the one your proxy actually sends. Most common proxies send the X-Forwarded-* family by default, so --proxy-headers=xforwarded is the usual value, but this is worth checking rather than assuming.
Two warnings attach to this option, both from the same page.
First, do not use it with TLS passthrough. The documentation is explicit: with passthrough “the proxy cannot inject forwarded headers into the encrypted traffic, and any headers already present in the request originate from the client and cannot be trusted.” Enabling the option there means accepting client-supplied headers as if a proxy had set them.
Second, restrict who is trusted. By default all addresses are trusted, and proxy-trusted-addresses takes “a comma separated list of trusted proxy addresses”, given as IP addresses or in CIDR notation. Once set, “proxy headers from other addresses will be ignored”. If Keycloak is reachable on the network by anything other than the proxy, set this.
The security consequence of getting this wrong is not cosmetic. The option’s own reference entry carries the warning that “misconfiguration might leave the server exposed to security vulnerabilities”, because a header a client controls becomes an input to URL generation and to address-based logic.
Symptom: Wrong Issuer, Wrong Discovery Endpoints, Broken Console Links
If tokens carry an internal hostname in iss, or the discovery document at /realms/<realm>/.well-known/openid-configuration advertises internal URLs, Keycloak is resolving its address dynamically from a request that no longer reflects the public URL.
The fix is the hostname option, documented as the “address at which is the server exposed”, which “can be a full URL, or just a hostname”. The distinction matters: “when only hostname is provided, scheme, port and context path are resolved from the request”, which puts you back in the same situation for those three components. Set a full URL when the deployment is behind anything.
Related options:
hostname-strict“disables dynamically resolving the hostname from request headers” and “should always be set to true in production, unless your reverse proxy overwrites the Host header”. Enabling it makeshostnamemandatory, which is the point: the pair together is what pins the address.hostname-adminis the “address for accessing the administration console”, for the case where the console is exposed through a reverse proxy on a different address thanhostname. If you set it,hostnamemust be a complete URL rather than a bare hostname.hostname-backchannel-dynamic“enables dynamic resolving of backchannel URLs, including hostname, scheme, port and context path”, for applications reaching Keycloak over a private network while the public address stays fixed. It also requireshostnameto be a full URL. The documentation warns that with TLS passthrough “the usage of dynamic backchannel HTTPS requests will not pass hostname validation as the server certificate presented will be for the public hostname”.
A useful detail when nothing makes sense: hostname-debug toggles a diagnostic page, documented as accessible at /realms/master/hostname-debug, that shows how the server is resolving its own address. Turning it on for a few minutes answers questions that log reading does not. Turn it off afterwards.
Symptom: Invalid Parameter, redirect_uri
This error comes from the client configuration rather than from server hostname settings, and the distinction is worth holding onto because the two get conflated constantly.
Keycloak matches the redirect_uri a client sends against the Valid Redirect URIs list on that client. The match is exact unless a wildcard is present, and the comparison includes scheme, host, port and path. The usual causes:
- The application was moved from HTTP to HTTPS and the registered URI still says
http://. - The port changed, or the public URL has no port while the registered value has one.
- A trailing slash exists on one side and not the other.
- The application sends a URI with a query string that the registered pattern does not permit.
Wildcards are convenient and worth using sparingly. A wildcard that is broader than the application’s real callback surface widens the set of places an authorization code can be delivered, which is the specific attack this check exists to prevent.
If the value being rejected looks correct, log what the client actually sent rather than what you believe it sent. Frameworks build the redirect URI from their own view of the request, so an application that is itself behind a proxy can construct an internal URI without anyone intending it.
Symptom: HTTPS Required
This message appears when the realm requires SSL and Keycloak believes the request arrived over plain HTTP. Behind a terminating proxy that is exactly what happens at the socket level, and the scheme is only recoverable from the forwarded headers.
So the first check is not the realm setting, it is whether proxy-headers is configured. If Keycloak is not reading the forwarding headers, it cannot know the original scheme, and relaxing the realm’s SSL requirement to make the error disappear replaces a configuration problem with a security one.
The production guidance is unambiguous that “all communication to and from Keycloak requires a secure communication channel”. Fix the header handling, not the requirement.
Symptom: Admin Console Loads Blank or Loops
Two causes account for most of these.
Relative path mismatch. If Keycloak is served under a sub-path, http-relative-path is the option that tells it so: “set the path relative to / for serving resources”, and “the path must start with a /”. If the proxy strips a prefix that Keycloak does not know about, the console’s asset URLs point somewhere that does not exist and the page renders empty.
Split hostnames. If the console is exposed on a different address than the token endpoints, hostname-admin needs to describe that address, and hostname needs to be a full URL for that combination to validate. Half-configuring it produces a console that loads its shell and then fails on its first API call.
An Ordered Checklist
When a deployment is misbehaving in any of the ways above, work through it in this order. Each step depends on the one before.
- Is Keycloak behind a proxy that terminates TLS? If yes, set
proxy-headerstoforwardedorxforwardedto match what the proxy sends. If it is TLS passthrough, do not set it at all. - Restrict trust. Set
proxy-trusted-addressesto the proxy addresses or CIDRs, since the default trusts every source. - Declare the public address. Set
hostnameto the full public URL, scheme and all, rather than a bare hostname. - Turn off dynamic resolution. Set
hostname-strictto true, which the documentation says should always be the case in production unless the proxy overwrites theHostheader. It requires step 3 to have been done. - Handle a separate admin address with
hostname-admin, remembering that it forceshostnameto be a complete URL. - Check the sub-path with
http-relative-pathif the proxy is not serving Keycloak from the root. - Verify from the outside. Fetch
/realms/<realm>/.well-known/openid-configurationover the public URL and confirm that every endpoint in it is a public URL. If any internal address appears there, the hostname configuration is still wrong regardless of what the console looks like. - Only then look at the client. Redirect URI errors that survive a correct step 7 are genuinely client configuration.
Step 7 is the one people skip, and it is the only one that tests what applications will actually receive. The discovery document is the server stating its own view of its address in machine-readable form, which makes it a better oracle than any page rendering correctly in a browser.
Getting It Right the First Time
Two habits prevent most of this class of failure.
Export realm configuration into version control so that a working hostname and client setup can be reproduced rather than rediscovered. The reasoning behind that, along with the realm and client structure it captures, is in Keycloak realms, clients and token design fundamentals.
Treat the proxy configuration as part of the Keycloak configuration rather than as network plumbing owned by someone else. The two have to agree about scheme, host, port and path, and there is no way to validate either half in isolation.
Once the deployment is reachable and issuing correct tokens, the next question is usually how much of it you need. Keycloak memory usage and heap sizing covers the documented sizing model, and the sizing calculator turns a login rate into a node and heap estimate. If the answer looks heavier than the requirement justifies, Keycloak vs authentik vs Authelia compares the alternatives on architecture and footprint.
Sources
Related
Keycloak Memory Usage and Heap Sizing Explained
What actually consumes Keycloak memory, the documented 1250 MB baseline, the 70 percent heap rule, and how to turn login rates into node sizing.
Keycloak vs authentik vs Authelia: What to Self-Host
A protocol, architecture and footprint comparison of three self-hosted identity servers, and the deployment shapes each one is actually built for.
Keycloak Realms, Clients and Token Design Fundamentals
How Keycloak realms, clients, flows and token lifetimes fit together, plus the caching, session and clustering decisions that bite operators later on.