Traefik + EAS + KeyCloak
CanisHelix opened this issue · comments
I am trying to setup EAS to front some lan services.
I created a config-token.json file with the following contents:
{
type: "oidc",
issuer: {
discover_url: "https://identity.mydomain.com/realms/test/.well-known/openid-configuration",
},
client: {
client_id: "eas",
client_secret: "supersecretkey"
},
scopes: ["openid", "email", "profile"],
pkce: {
enabled: false,
code_challenge_method: "S256"
},
nonce: {
enabled: false,
ttl: 600
},
custom_authorization_parameters: {},
custom_authorization_code_parameters: {},
custom_refresh_parameters: {},
custom_revoke_parameters: {},
redirect_uri: "https://eas.mydomain.com/oauth/callback",
features: {
cookie_expiry: false,
userinfo_expiry: true,
session_expiry: true,
session_expiry_refresh_window: 86400,
session_retain_id: true,
refresh_access_token: true,
fetch_userinfo: true,
introspect_access_token: false,
introspect_expiry: 0,
authorization_token: "access_token",
filtered_service_headers: [],
logout: {
revoke_tokens_on_logout: [],
"end_provider_session": {
"enabled": false,
"post_logout_redirect_uri": "https://eas.mydomain.com/oauth/end-session-redirect"
},
"backchannel": {
"enabled": false
},
},
},
assertions: {
aud: true,
exp: true,
nbf: true,
iss: true,
sig: {
enabled: false,
},
},
xhr: {
},
csrf_cookie: {
},
cookie: {
},
custom_error_headers: {},
custom_service_headers: {},
}
Using the following command to get a token:
cat config-token.json | docker run --rm -i \
-e EAS_CONFIG_TOKEN_SIGN_SECRET=qvU8f5pDkN \
-e EAS_CONFIG_TOKEN_ENCRYPT_SECRET=ihk4SMaXkd \
-e EAS_ISSUER_SIGN_SECRET=ySNNaneauA \
-e EAS_ISSUER_ENCRYPT_SECRET=3Yu6ntRMiY \
-e EAS_COOKIE_SIGN_SECRET=3xibFU30rn \
-e EAS_COOKIE_ENCRYPT_SECRET=KeHuSHjGEi \
-e EAS_SESSION_ENCRYPT_SECRET=w4lPDX8AC2 \
travisghansen/external-auth-server generate-config-token
In my traefik.toml I have my LAN and Docker Networks set to trustedIP's for forwarding headers. (Prior to this I had missing X-Forward-URI errors)
[entryPoints.web]
address = ":80"
[entryPoints.web.http]
[entryPoints.web.http.redirections]
[entryPoints.web.http.redirections.entryPoint]
to = "websecure"
scheme = "https"
# Enable Forwarded Headers
[entryPoints.web.forwardedHeaders]
trustedIPs = ["10.10.2.0/24","172.16.0.0/12"]
[entryPoints.websecure]
address = ":443"
[entryPoints.websecure.http.tls]
certResolver = "lets-encrypt"
# Enable Forwarded Headers
[entryPoints.websecure.forwardedHeaders]
trustedIPs = ["10.10.2.0/24","172.16.0.0/12"]
In my traefik_dynamic.toml I have the backends for Keycloak/EAS and Firefly setup as follows:
# Hosts
[http.routers]
# Identity Provider
[http.routers.identity]
rule = "Host(`identity.mydomain.com`) && ClientIP(`10.23.2.0/24`,`172.16.0.0/12`)"
entrypoints = ["websecure"]
service = "identity"
[http.routers.identity.tls]
certResolver = "lets-encrypt"
[[http.routers.identity.tls.domains]]
main = "mydomain.com"
sans = ["*.mydomain.com"]
[http.routers.eas]
rule = "Host(`eas.mydomain.com`) && ClientIP(`10.23.2.0/24`,`172.16.0.0/12`)"
entrypoints = ["websecure"]
service = "eas"
[http.routers.eas.tls]
certResolver = "lets-encrypt"
[[http.routers.eas.tls.domains]]
main = "mydomain.com"
sans = ["*.mydomain.com"]
[http.routers.firefly]
rule = "Host(`terra.mydomain.com`) && ClientIP(`10.23.2.0/24`,`172.16.0.0/12`)"
entrypoints = ["websecure"]
service = "firefly"
middlewares = "eas"
[http.routers.firefly.tls]
certResolver = "lets-encrypt"
[[http.routers.firefly.tls.domains]]
main = "mydomain.com"
sans = ["*.mydomain.com"]
# Backends
[http.services]
# Identity Provider
[http.services.identity.loadBalancer]
[[http.services.identity.loadBalancer.servers]]
url = "http://server1.lan:8004"
[http.services.eas.loadBalancer]
[[http.services.eas.loadBalancer.servers]]
url = "http://server1.lan:8181"
[http.services.firefly.loadBalancer]
[[http.services.firefly.loadBalancer.servers]]
url = "http://server1.lan:8003"
# Middlewares
[http.middlewares]
[http.middlewares.eas.forwardAuth]
address = "https://eas.mydomain.com/verify?config_token=<trunc>"
trustForwardHeader = true
authResponseHeaders = ["X-Auth-Username", "X-Auth-Email", "X-Forwarded-Uri", "X-Forwarded-User", "X-Access-Token", "Authorization"]
Unfortunately navigating to terra.mydomain.com results in a 503, no prompt for Keycloak at all and EAS's docker (with silly log level) reports the following:
error: error:0606506D:digital envelope routines:EVP_DecryptFinal_ex:wrong final block length {"stack":"Error: error:0606506D:digital envelope routines:EVP_DecryptFinal_ex:wrong final block length\n at Object.decrypt (/home/eas/app/src/utils.js:106:11)\n at _verifyHandler (/home/eas/app/src/server.js:138:46)\n at verifyHandler (/home/eas/app/src/server.js:94:18)\n at Layer.handle [as handle_request] (/home/eas/app/node_modules/express/lib/router/layer.js:95:5)\n at next (/home/eas/app/node_modules/express/lib/router/route.js:144:13)\n at next (/home/eas/app/node_modules/express/lib/router/route.js:140:7)\n at next (/home/eas/app/node_modules/express/lib/router/route.js:140:7)\n at next (/home/eas/app/node_modules/express/lib/router/route.js:140:7)\n at next (/home/eas/app/node_modules/express/lib/router/route.js:140:7)\n at next (/home/eas/app/node_modules/express/lib/router/route.js:140:7)","timestamp":"2023-01-29T14:28:15.969Z"}
My docker-compose.yml is and as you can see the secret's are the same.
eas:
container_name: eas
image: travisghansen/external-auth-server:latest
environment:
- EAS_PORT=8181
- EAS_CONFIG_TOKEN_SIGN_SECRET=qvU8f5pDkN
- EAS_CONFIG_TOKEN_ENCRYPT_SECRET=ihk4SMaXkd
- EAS_ISSUER_SIGN_SECRET=ySNNaneauA
- EAS_ISSUER_ENCRYPT_SECRET=3Yu6ntRMiY
- EAS_COOKIE_SIGN_SECRET=3xibFU30rn
- EAS_COOKIE_ENCRYPT_SECRET=KeHuSHjGEi
- EAS_SESSION_ENCRYPT_SECRET=w4lPDX8AC2
- LOG_LEVEL=silly
ports:
- 8181:8181/tcp
restart: always
After a day of this I'm stuck as to what could be the problem here. Anything I have done wrong in the above?
Welcome! Sorry it’s been such a struggle :( I’m sure we can get you going though.
So generally the process you followed looks sane. The structure of your json is off a little bit and what you sent seems like invalid json as well. But that may not be your issue currently anyway. Can you send the output/config token data from running the docker run command?
And send over the logs previous to the 503 line/error as well.
While waiting for further details, here are some notes:
- the structure of the json is off, use something like the following (you can use yaml vs json):
aud: someaudience
eas:
plugins:
- type: oidc
issuer:
discover_url: >-
https://identity.mydomain.com/realms/test/.well-known/openid-configuration
client:
client_id: eas
client_secret: supersecretkey
scopes:
- openid
- email
- profile
pkce:
enabled: false
code_challenge_method: S256
nonce:
enabled: false
ttl: 600
custom_authorization_parameters: {}
custom_authorization_code_parameters: {}
custom_refresh_parameters: {}
custom_revoke_parameters: {}
redirect_uri: 'https://eas.mydomain.com/oauth/callback'
features:
cookie_expiry: false
userinfo_expiry: true
session_expiry: true
session_expiry_refresh_window: 86400
session_retain_id: true
refresh_access_token: true
fetch_userinfo: true
introspect_access_token: false
introspect_expiry: 0
authorization_token: access_token
filtered_service_headers: []
logout:
revoke_tokens_on_logout: []
end_provider_session:
enabled: false
post_logout_redirect_uri: 'https://eas.mydomain.com/oauth/end-session-redirect'
backchannel:
enabled: false
assertions:
aud: true
exp: true
nbf: true
iss: true
sig:
enabled: false
xhr: {}
csrf_cookie: {}
cookie: {}
custom_error_headers: {}
custom_service_headers: {}
- if your keycloak env supports it I would suggest enabling
nonce
andpkce
, they provide the best security - (this is likely your primary issue) the
config_token
as returned using the command above is not url encoded. Run the token through your encoder of choice or something like https://www.urlencoder.org/ to get the values as it should be in your traefik config (ie:address = "https://eas.mydomain.com/verify?config_token=<THE ENCODED STRING HERE>"
- ensure that the
eas
is not protected by theeas
middleware configuration...that will not end well :)
Thanks for the quick feedback.
I ripped the config_token straight from an example in HOWTO.md (https://github.com/travisghansen/external-auth-server/blob/master/HOWTO.md#generage-config_token) but at a late hour I clearly mistook Javascript for JSON. But then I expected the generate-config-token command to detect that and produce an error instead of providing a token back.
I've corrected that now, and used https://www.urlencoder.org/ and now it is working as expected.
Regarding PKCE/Nonce, I'm still new to Keycloak (latest version) and not even sure how to enable these yet on the realm? client? server?
With the advice given I've almost got this working. The end service is expecting the email address in a single header containing only an email address. But X-Auth-Email and X-Auth-Username are empty, X-Userinfo (I added this as a forwarded header to Traefik) is passed along, but the end service needs just the email.
So the only challenge is figuring out how to get just the Email into a header via Traefik/Keycloak/EAS. Still new to SSO so learning lot's here.
Yup! This project is super powerful but also takes a minute to grok all the config. I can send you exact syntax but I'll need you to send over samples of the id_token/userinfo data so I know exactly where in the data the relevant datapoints reside. The mechanism for doing this is the custom_service_headers
feature.
This is super powerful, but definitely takes some time to understand the configuration. Just found custom_service_headers and reading some examples.
The X-Userinfo being passed looks something like this:
{"sub":"991fed2d-f5ed-490e-88b5-268100b63ebe","email_verified":true,"name":"FirstName LastName","preferred_username":"firstname","given_name":"FirstName","family_name":"LastName","email":"me@mydomain.com"}
So I'm thinking something like this might work:
"custom_service_headers": {
"X-Auth-Email": {
"source": "userinfo",
"encoding": "plain",
"query_engine": "jq",
"query": "[ .emails[].email ] | first",
},
"X-Auth-Username": {
"source": "userinfo",
"encoding": "plain",
"query_engine": "jq",
"query": "[ .preferred_username ]",
}
}
For best performance use jp
query_engine
:
Something like this should make it all work:
"custom_service_headers": {
"X-Auth-Email": {
"source": "userinfo",
"encoding": "plain",
"query_engine": "jp",
"query": "$.email",
"query_opts": {
"single_value": true
}
},
"X-Auth-Username": {
"source": "userinfo",
"encoding": "plain",
"query_engine": "jp",
"query": "$.preferred_username",
"query_opts": {
"single_value": true
}
}
}
That works perfectly too. Thanks ever so much with the help in getting this up and running.
No problem! Enjoy!
Another possibility is to include email
in your scopes and use the data from the id_token
instead of userinfo
as userinfo
requires an api call to the provider to get the extra data. eas
can/will cache userinfo
data to keep the overhead down but if the needs are very slim it may make sense to go that route.