How to Run the Keycloak Admin Console on a Separate Hostname Behind nginx (KC_HOSTNAME_ADMIN)

Guilliano Molaire Guilliano Molaire 10 min read

Last updated: June 2026


KC_HOSTNAME_ADMIN does work behind nginx, and the fix is almost always your proxy config, not Keycloak. Set KC_HOSTNAME and KC_HOSTNAME_ADMIN to your two URLs, set KC_PROXY_HEADERS=xforwarded, then route a second nginx server block to Keycloak that forwards its own host as X-Forwarded-Host. That second hostname, with the right forwarded headers, is the whole trick. Most “it doesn’t work” reports trace back to a custom /admin/ location that quietly drops those headers, so Keycloak falls back to the backend host and the console builds broken URLs.

If you have read that KC_HOSTNAME_ADMIN is broken behind a reverse proxy, that is understandable. The threads are full of blank admin pages, redirect loops, and busted asset URLs. The good news: every one of those symptoms is a proxy-header problem, not a Keycloak bug. We reproduced the working setup on a live Keycloak 26.3 + Postgres + nginx stack, confirmed the admin console loads on its own hostname, and pinned down exactly why it breaks for most people. Here is the config that works and the reasoning behind it.

This post assumes you already have Keycloak running behind nginx. If you do not, read how to run Keycloak behind a reverse proxy first for the base setup. We will not re-teach X-Forwarded header theory here.

The working configuration

The setup has two halves: a small set of Keycloak environment variables, and two nginx server blocks (one per hostname). Get both right and the admin console lands on its own domain. Here is the Keycloak side, using the hostname v2 options from Keycloak 26.x.

KC_HOSTNAME=https://login.example.com
KC_HOSTNAME_ADMIN=https://admin.example.com
KC_HTTP_ENABLED=true
KC_PROXY_HEADERS=xforwarded

KC_HOSTNAME is your frontend (where users log in). KC_HOSTNAME_ADMIN is the separate URL where the admin console and admin REST endpoints will be advertised. KC_HTTP_ENABLED=true lets Keycloak listen on plain HTTP because nginx terminates TLS. KC_PROXY_HEADERS=xforwarded is the one people forget: without it, Keycloak ignores every X-Forwarded-* header you send.

Now nginx. You need two server blocks, one per hostname, and both must forward the original host. The key detail is that each block sends its own $host as X-Forwarded-Host, so admin requests arrive carrying admin.example.com.

# Frontend: where users log in
server {
    listen 443 ssl;
    server_name login.example.com;

    ssl_certificate     /etc/nginx/certs/login.example.com.crt;
    ssl_certificate_key /etc/nginx/certs/login.example.com.key;

    location / {
        proxy_pass http://keycloak:8080;
        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host  $host;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    }
}

# Admin: the separate console hostname
server {
    listen 443 ssl;
    server_name admin.example.com;

    ssl_certificate     /etc/nginx/certs/admin.example.com.crt;
    ssl_certificate_key /etc/nginx/certs/admin.example.com.key;

    location / {
        proxy_pass http://keycloak:8080;
        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host  $host;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    }
}

Notice both blocks proxy to the same Keycloak backend, and both use an identical proxy_set_header block. Because each request carries the hostname it actually came in on, Keycloak can tell a login request (login.example.com) from an admin request (admin.example.com) and generate the right URLs for each. No /admin/ path juggling required. You route a hostname, not a path.

Point your DNS for both names at nginx, give each a TLS certificate, and you are done. On our test stack the admin console came up at https://admin.example.com/admin/master/console/ with an HTTP 200, while the login flows stayed on https://login.example.com.

What Keycloak does with this

With the config above, Keycloak splits its generated URLs cleanly between the two hostnames. We confirmed this by inspecting the console’s runtime config on the live 26.3 stack. The frontend issuer stays on the login host, and the admin surfaces move to the admin host. That split is the entire point of KC_HOSTNAME_ADMIN.

Here is the verified breakdown. The realm issuer, the value clients see in tokens, resolves to https://login.example.com/realms/master. Inside the admin console’s bootstrap config, Keycloak sets adminBaseUrl and authUrl to https://admin.example.com, while serverBaseUrl and authServerUrl point at https://login.example.com.

authServerUrl  = https://login.example.com   # token/server endpoints
serverBaseUrl  = https://login.example.com   # frontend-facing base
authUrl        = https://admin.example.com   # admin console auth
adminBaseUrl   = https://admin.example.com   # admin console assets/API

Read that split slowly, because it is the part people miss. The admin console is a single-page app served from the admin host, but it still authenticates against, and talks to, the frontend server endpoints for token issuance. Keycloak handles that handoff for you once the hostnames are advertised correctly. We then ran a real admin-cli password grant through the admin host: it returned a token, and GET /admin/realms returned the realm list. The full admin path works end to end, not just the static page load.

Why it usually fails behind nginx

If you set KC_HOSTNAME_ADMIN and got a blank page or a redirect loop, you hit one of three things. In our testing, the first one is by far the most common, and it is sneaky because the config looks correct.

The custom /admin/ location footgun

This is the big one, and Nginx Proxy Manager users walk into it constantly. The instinct is to add a “custom location” for /admin/, something like location ^~ /admin/ { proxy_pass ...; }, with its own proxy directives. The problem: an nginx location block does not inherit proxy_set_header from its parent. The moment you declare even one proxy_set_header inside that /admin/ block, all the inherited ones vanish.

So your admin requests silently lose X-Forwarded-Host and X-Forwarded-Proto. Keycloak, told to trust forwarded headers but receiving none, falls back to the backend host (something like keycloak:8080). The console then builds asset and redirect URLs against that backend host. The result is the exact symptom everyone reports: a blank console, a redirect loop, or assets that 404 because they point at an unreachable internal name.

The fix is to stop fighting paths. With KC_HOSTNAME_ADMIN you do not need an /admin/ location at all. Route a whole second hostname, as shown above, and let every request in that server block carry the correct forwarded headers. One host, one consistent header set, no inheritance surprises.

Not actually routing a second hostname

The second failure is subtler: people set the environment variable and expect magic. But KC_HOSTNAME_ADMIN only changes which URLs Keycloak generates. It does nothing to traffic routing. If admin.example.com does not resolve to nginx, or nginx has no server block for it, the hostname is a dead end no matter what Keycloak advertises. You still need the admin server block, with forwarded headers, sitting between DNS and Keycloak.

KC_PROXY_HEADERS not set

The third one is quick. If KC_PROXY_HEADERS is unset, Keycloak ignores X-Forwarded-* entirely as a security default, so your carefully forwarded host never gets used. Set KC_PROXY_HEADERS=xforwarded (or forwarded if you forward the standard Forwarded header instead). This is covered in depth in our reverse proxy guide, so we will not belabor it here.

KC_HOSTNAME_ADMIN is not access control

Here is the caveat that bites teams who assume a separate admin hostname means a locked admin surface. It does not. KC_HOSTNAME_ADMIN changes URL generation only. It is not a security boundary.

We verified this directly: even with the split config in place, the admin console and the admin REST API (/admin/realms) remained reachable on the frontend host too, returning HTTP 200 on both login.example.com and admin.example.com. Keycloak does not refuse /admin requests that arrive on the frontend hostname; it just prefers to advertise admin URLs on the admin host. Anyone who knows the path can still hit /admin on your public login domain.

So if your goal is to genuinely restrict admin access to a separate or internal-only host, you need two things: KC_HOSTNAME_ADMIN for correct URL generation, plus nginx rules that lock the admin surface down. The next section shows the exact config we tested. If you are hardening a whole deployment, the Keycloak production-ready checklist is a good companion read.

Locking the admin subdomain to local-only

This is the companion config that turns the separate hostname into an actually restricted one. It is two changes, and we verified both on the same test stack.

First, block /admin on the public frontend server block, so neither the console nor the admin REST API is exposed there:

# in the login.example.com (public) server block
location /admin/ { return 403; }

That is safe for end users. Their login pages and token endpoints live under /realms/, not /admin/. In our test, blocking /admin/ on the public host returned 403 for both the console and /admin/realms, while end-user OIDC discovery and token grants kept returning 200. Login traffic is untouched.

Second, restrict the admin server block to your internal network:

# in the admin.example.com server block
allow 192.168.0.0/16;
allow 10.0.0.0/8;
deny  all;

One catch that quietly defeats this: if another proxy sits in front of this nginx (Nginx Proxy Manager, Cloudflare, a load balancer), allow/deny evaluates the connection’s IP, which is the upstream proxy, not the real client. Every request would look like it came from the proxy. Restore the real client IP first:

set_real_ip_from 10.0.0.0/8;   # the network your upstream proxy sits on
real_ip_header   X-Forwarded-For;

For a homelab “local only,” the cleanest option is often to not give the admin subdomain a public route at all: publish it on internal DNS (split-horizon) or put it behind a VPN such as Tailscale. For per-path allowlisting on a single host, or finer-grained rules, see path-based IP restriction for the Keycloak admin console.

FAQ

Does KC_HOSTNAME_ADMIN work behind nginx?

Yes. We confirmed it on Keycloak 26.3 + nginx: the admin console loads at its own hostname with an HTTP 200, and admin REST calls succeed. The fix when it “doesn’t work” is almost always the proxy config, specifically missing X-Forwarded-Host/X-Forwarded-Proto headers or an unset KC_PROXY_HEADERS, not a Keycloak limitation.

Do I need a separate nginx location for /admin?

No, and you should avoid it. A separate /admin/ location is the most common cause of failures, because nginx location blocks do not inherit proxy_set_header from the parent. Once you route a second hostname with KC_HOSTNAME_ADMIN, you forward a whole server block instead of a path, which keeps the forwarded headers consistent.

Why is my Keycloak admin console blank or stuck in a redirect loop?

The console almost always renders blank or loops because it built asset and redirect URLs against the internal backend host (like keycloak:8080) instead of your public hostname. That happens when X-Forwarded-Host/X-Forwarded-Proto are missing or KC_PROXY_HEADERS=xforwarded is not set, so Keycloak never sees the real hostname.

What is the difference between KC_HOSTNAME and KC_HOSTNAME_ADMIN?

KC_HOSTNAME sets the frontend URL where users authenticate and where token issuers resolve. KC_HOSTNAME_ADMIN sets a separate URL for the admin console and admin endpoints. With both set, Keycloak puts authServerUrl and serverBaseUrl on the frontend host while adminBaseUrl and authUrl move to the admin host. The split is purely about generated URLs.

Does KC_HOSTNAME_ADMIN block admin access on my public domain?

No. This is a verified gotcha: KC_HOSTNAME_ADMIN only changes URL generation, not access. The admin console and /admin/realms stay reachable on the frontend host too (HTTP 200 on both). To actually lock admin to one host, add an nginx rule that blocks /admin on the public domain on top of setting the admin hostname.

How do I restrict the Keycloak admin console to my local network?

Use two nginx rules on top of KC_HOSTNAME_ADMIN: block /admin/ on the public frontend server block (location /admin/ { return 403; }), and add allow/deny rules to the admin server block so only your LAN ranges reach it. If a proxy sits in front of nginx, configure set_real_ip_from and real_ip_header X-Forwarded-For first, or the allow/deny rules only see the proxy IP. We verified that blocking /admin/ on the public host leaves end-user login and token endpoints (under /realms/) working normally.

Do I still need KC_PROXY_HEADERS if I set KC_HOSTNAME_ADMIN?

Yes, always. Without KC_PROXY_HEADERS=xforwarded, Keycloak ignores every X-Forwarded-* header for safety, including the host you forward for the admin domain. The admin hostname split only works when Keycloak is allowed to read the forwarded host, so the two settings go together.

Summary

KC_HOSTNAME_ADMIN behind nginx is not broken, it is just unforgiving about proxy headers. Set KC_HOSTNAME, KC_HOSTNAME_ADMIN, KC_HTTP_ENABLED=true, and KC_PROXY_HEADERS=xforwarded, then give Keycloak a dedicated nginx server block per hostname that forwards its own X-Forwarded-Host. Skip the custom /admin/ location entirely; route a hostname, not a path. Keycloak then splits its URLs cleanly: login and token endpoints on the frontend host, admin console and admin API on the admin host. Just remember the split is cosmetic for access purposes: admin remains reachable on the frontend host unless you add an explicit nginx block for it.

Want this kind of hostname split, proxy headers, and admin hardening handled for you, on a Keycloak instance you do not have to babysit? That is what we do. Take a look at Skycloak managed Keycloak hosting.

Guilliano Molaire
Written by Guilliano Molaire Founder

Guilliano is the founder of Skycloak and a cloud infrastructure specialist with deep expertise in product development and scaling SaaS products. He discovered Keycloak while consulting on enterprise IAM and built Skycloak to make managed Keycloak accessible to teams of every size.

Ready to simplify your authentication?

Deploy production-ready Keycloak in minutes. Unlimited users, flat pricing, no SSO tax.

© 2026 Skycloak. All Rights Reserved. Design by Yasser Soliman