Protecting an identity-provider admin console at the edge, the secure way
Your identity provider has one hostname. Customers log in on it, and the admin console that can create users, reset MFA and mint tokens answers on the same hostname, to the same internet, with only a password in the way.
The short answer
Split the identity provider's paths at the edge. Send the admin paths (for Keycloak, /admin and /realms/master) to their own HTTPRoute and attach a SecurityPolicy with defaultAction Deny that allows only your admin network. Keep the login realms public. Let Envoy Gateway's path normalization stay on so encoded and dotted paths cannot slip past the match.
On this page
What goes wrong
An identity provider (IdP) serves two audiences from one server. End users need the login pages, the token endpoints and the discovery document. A few administrators need the admin console and its API. Behind a reverse proxy that forwards everything, both audiences reach both.
The admin console is the most valuable page you run. Whoever controls it can add users, remove MFA, change redirect URIs and issue tokens for every application behind the IdP. Exposing it means its login form, its session handling and every future bug in them are one request away from anyone.
The naive fix, a path rule on the proxy, has its own traps: a rule that
matches /admin but not //admin, /%61dmin or /realms/x/../../admin,
and a forgotten second admin path. In Keycloak, the admin console logs in
through the master realm, so /realms/master is an admin path too.
What the docs say
Exposed admin paths lead to an unnecessary attack vector.
Source: Keycloak docs, Configuring a reverse proxy, exposed path recommendations
Assuming the administrative realm is called master , restrict /realms/master/ to internal access.
Source: Keycloak docs, Configuring a reverse proxy, exposed path recommendations
Note: if neither Rules nor DefaultAction is specified, the default action is to deny all requests.
Source: Envoy Gateway API reference, Authorization
Keycloak's docs name the paths. They leave the enforcement to your proxy and do not describe the path variants a proxy must normalize.
The secure configuration
Client IP detection first, so the allowlist sees real addresses (one load balancer in front here):
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: ClientTrafficPolicy
metadata:
name: client-ip
namespace: edge
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: edge
clientIPDetection:
xForwardedFor:
numTrustedHops: 1Envoy Gateway attaches one ClientTrafficPolicy per Gateway. If the Gateway
already has one (for TLS settings, for example), add clientIPDetection to
that policy instead of creating a second; a second one is refused with
Accepted=False (see ML-KEM-1024 on Envoy Gateway).
A route for the admin paths only, and the allowlist on it:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: idp-admin
namespace: edge
spec:
parentRefs:
- name: edge
hostnames: ["id.example.com"]
rules:
- matches:
- path: {type: PathPrefix, value: /admin} # console and admin REST API
- path: {type: PathPrefix, value: /realms/master} # the admin realm's login
backendRefs:
- name: keycloak
port: 8080
---
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: SecurityPolicy
metadata:
name: idp-admin-allowlist
namespace: edge
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: HTTPRoute
name: idp-admin
authorization:
defaultAction: Deny # everyone not listed below
rules:
- name: admin-network
action: Allow
principal:
clientCIDRs:
- 198.51.100.0/24 # VPN or tailnet egress for administratorsThe public route (/realms/<your realm>, /resources, /.well-known, or
simply / for the rest) stays as it is. Envoy Gateway orders the rendered
routes so the longer prefixes are matched before /, so the admin prefixes
always land on the protected route.
This route-level policy replaces any Gateway-level SecurityPolicy on the
admin paths, including a blocklist or WAF bans, even with
mergeType: StrategicMerge: when both set authorization, the route's rules
win. In the lab, an address banned at the Gateway still reached the console
because it was also in the admin network. If you ban at the Gateway, copy the
deny rules into this policy, before the allow rule (see
WAF auto-ban across PoPs).
Do not turn off Envoy Gateway's path handling (normalizePath,
mergeSlashes, UnescapeAndRedirect for escaped slashes). It is what makes
the prefix match hold against encoded and dotted paths.
Prove it
What Envoy Gateway renders (offline, no cluster)
The test in secure-tests/protect-idp-admin-console-edge/run.sh renders the
three resources with egctl:
== Envoy Gateway v1.9.1, rendered offline by egctl
SecurityPolicy Accepted="True": Policy has been accepted.
pathSeparatedPrefix: /realms/master
- addressPrefix: 198.51.100.0
action: DENY
pathSeparatedPrefix: /admin
- addressPrefix: 198.51.100.0
action: DENY
prefix: /
mergeSlashes: true
normalizePath: true
pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECTThe rendered config on Envoy 1.39.1
The same script runs those routes, the RBAC matcher and the path settings on
a standalone Envoy, with stand-in responses for the IdP. The client plays the
load balancer and sets the client address in X-Forwarded-For. curl --path-as-is sends the paths exactly as written.
== Envoy 1.39.1 with the rendered config; the client plays the load balancer (XFF = client address)
198.51.100.5 /admin/master/console/ -> 200 admin console
203.0.113.9 /admin/master/console/ -> 403 RBAC: access denied
203.0.113.9 /realms/master/protocol/openid-connect/auth -> 403 RBAC: access denied
203.0.113.9 /realms/customers/protocol/openid-connect/auth -> 200 public
203.0.113.9 //admin/master/console/ -> 403 RBAC: access denied
203.0.113.9 /realms/customers/../../admin/master/console/ -> 403 RBAC: access denied
203.0.113.9 /%61dmin/master/console/ -> 403 RBAC: access denied
203.0.113.9 /admin%2Fmaster/console/ -> 307
203.0.113.9 /ADMIN/master/console/ -> 200 publicThe admin network gets the console; everyone else gets 403 on both admin
paths and still reaches the customer realm. Double slashes, dot segments and
an encoded a are normalized before the match. The escaped slash is
redirected to its unescaped form, which then hits the rule. The last line is
the one to check on your IdP: path matching is case-sensitive, so /ADMIN
goes to the public route. That is only safe if the IdP also treats paths
case-sensitively. Send the request through to your IdP and check that it
answers 404, not the console.
On a cluster, in front of Keycloak
The same resources on a lab cluster, in front of a real Keycloak 26.4.0
(start-dev), with a second HTTPRoute sending the rest of id.example.com
to Keycloak. The admin network was one test pod's address (other); the
requests went through a proxy that appends X-Forwarded-For.
$ kubectl get securitypolicy idp-admin-allowlist -n edge -o jsonpath='...'
Accepted=True Policy has been accepted.
other /admin/master/console/ -> 200 <title>Keycloak Administration Console</title>
other2 /admin/master/console/ -> 403 RBAC: access denied
other2 /realms/master/.well-known/openid-configuration -> 403 RBAC: access denied
other2 /realms/master/protocol/openid-connect/auth -> 403 RBAC: access denied
other2 /admin/realms/master/users -> 403 RBAC: access denied
other2 //admin/master/console/ -> 403 RBAC: access denied
other2 /realms/customers/../../admin/master/console/ -> 403 RBAC: access denied
other2 /%61dmin/master/console/ -> 403 RBAC: access denied
other2 /admin%2Fmaster/console/ -> 307 (location: /admin/master/console/, then 403)
other2 /ADMIN/master/console/ -> 404 "error":"Unable to find matching target resource method"
other2 /Admin/master/console/ -> 404 "error":"Unable to find matching target resource method"
other2 /realms/MASTER/.well-known/openid-configuration -> 404 "error":"Realm does not exist"
other2 /realms/Master/protocol/openid-connect/auth -> 404 "error":"Realm does not exist"
other2 /realms/customers/.well-known/openid-configuration -> 404 (public route: reached Keycloak)The case-changed paths pass the edge, as expected, and Keycloak 26.4.0
answers them with 404: it matches paths and realm names case-sensitively,
so there is no console behind /ADMIN. Repeat this check after every IdP
upgrade and for any other IdP you put behind the same pattern.
On your cluster, from outside the admin network:
curl -s -o /dev/null -w "%{http_code}\n" https://id.example.com/admin/master/console/
curl -s -o /dev/null -w "%{http_code}\n" https://id.example.com/realms/master/.well-known/openid-configuration
curl -s -o /dev/null -w "%{http_code}\n" https://id.example.com/ADMIN/master/console/What you should see: 403 for the first two and 404 for the third. From
the admin network, the console loads.
Mistakes people make
Protecting /admin and forgetting the admin realm
In Keycloak the console authenticates through /realms/master. If that
realm stays public, its login form and password reset are exposed. Other
IdPs have their own second path (for example a console path and a
management API path); list them all.
An allowlist that sees the load balancer
Without client IP detection every request comes from the load balancer, and an allowlist either blocks administrators or, once someone adds the load balancer's range, allows everyone.
Turning off path normalization to fix an app bug
normalizePath and mergeSlashes are what keep //admin and /%61dmin on
the protected route. Fix the app instead of weakening the edge.
Relying on the allowlist alone
The allowlist reduces who can try. Administrators still need phishing resistant MFA, and the admin realm should have its own strong policies. See admin tools reachable only on a tailnet for a stronger network boundary.
Serving admin on the public hostname at all
Better still: give administration its own hostname on an internal Gateway or a private network, as Keycloak supports with a separate admin hostname, so the public Gateway never routes admin paths at all.
Checklist
- List every admin path of your IdP (for Keycloak:
/admin,/realms/master). - Configure client IP detection before any allowlist.
- Put the admin paths on their own HTTPRoute.
- Attach a SecurityPolicy with
defaultAction: Denyand anAllowrule for the admin network. - Keep Envoy Gateway's path normalization on.
- Test
//admin,/%61dmin, dot segments and upper case from outside. - Move administration to a separate internal hostname when you can.
Your customers need the front door. Your admin console needs a door with no handle on the outside.
H2-CIAE
Learn it on a live range
SSO and lifecycle, in Identity and Access Engineering: a real host in your browser, and every objective checked on the machine.
Start freeThe Dome
Want it run for you?
The Dome puts post-quantum TLS, a WAF that blocks, signed DNS and a zero-trust mesh in front of your application. Tell us what you run.
See the Dome