Sign-in
Eddy works out who a person is and which groups they are in. It never decides what they may do: every cluster read and write is then impersonated as that user and those groups, and each cluster's own RBAC has the final say. This page sets up the sign-in side.
Full reference: docs/auth.md in the repository.
Choose a method
| Method | Config | Best for |
|---|---|---|
| GitHub | config.auth.github | Teams organised in GitHub organizations and teams (github.com or Enterprise Server) |
| OpenID Connect | config.auth.oidc[] | Google Workspace, Okta, Entra ID, Dex, Keycloak or any OIDC provider; several at once |
| Local users | config.auth.local | Air-gapped installs, kind, or a break-glass admin next to a provider |
| Trusted proxy | config.auth.proxy / oauth2Proxy | An existing oauth2-proxy or Pomerium in front of your tools |
You can enable several at once. The sign-in page shows a Continue with … button per provider. With exactly one provider and passwords off, it goes straight to that provider. After a failed sign-in it shows a reason and never redirects on its own, so a broken provider cannot loop.
For GitHub and OIDC, Eddy uses the OAuth authorization code flow with PKCE. Register this callback URL at the provider for each one you enable (the chart's install notes print them):
https://eddy.internal.example.com/auth/github/callbackhttps://eddy.internal.example.com/auth/<oidc id>/callback
publicURL must be exactly the address people type in the browser.
GitHub
Eddy can sign people in with a GitHub App (recommended) or an OAuth App. Both use the same flow and give the same result:
- User name: the primary, verified email of the account, in lower case, or
github:<login>if there is none. - Groups:
github:<org>for each organization the user is an active member of, andgithub:<org>/<team-slug>for each team. Only organizations inallowedOrganizationscount. With theeddy:prefix, the teamacme/platformbecomeseddy:github:acme/platform. - Who may sign in: members of at least one organization in
allowedOrganizations, which is required. WithallowedTeams(org/team-slug) they must also be in one of those teams.
Create a GitHub App, step by step
You need to be an owner of the organization, or have the GitHub App manager role.
- Open the app settings. In GitHub, open your organization, then Settings → Developer settings → GitHub Apps → New GitHub App. On GitHub Enterprise Server the path is the same on your server. To own the app personally, use your account's Settings → Developer settings → GitHub Apps.
- Fill in the basics.
- GitHub App name: for example
Eddy (acme). - Homepage URL:
https://eddy.internal.example.com(yourpublicURL). - Callback URL:
https://eddy.internal.example.com/auth/github/callback. - Leave Expire user authorization tokens checked. Leave Request user authorization (OAuth) during installation and Enable Device Flow unchecked.
- Webhook: uncheck Active. Eddy needs no webhooks.
- GitHub App name: for example
- Set the permissions. Everything else stays at No access.
- Organization permissions → Members: Read-only, for memberships and teams.
- Account permissions → Email addresses: Read-only, for the primary verified email.
- Choose where it can be installed. Pick Only on this account, then click Create GitHub App.
- Note the client ID on the app's page. It starts with
Iv(it is not the numeric App ID). - Generate a client secret. Under Client secrets, click Generate a new client secret and copy it now, because GitHub shows it once. You do not need a private key: Eddy never acts as the app itself.
- Install the app in the organization: Install App in the left menu, then Install next to the organization. No repository access is requested. Without the installation, the user's token cannot see the organization and sign-in is denied. Repeat for each organization in
allowedOrganizations. - Put the secret in the Kubernetes Secret. Add it to
eddy-credentialsunder the keyGITHUB_CLIENT_SECRET, with the add-a-key command. The chart loadscredentialsSecretas environment variables. - Configure the chart in
hub-values.yaml:Then upgrade:publicURL: https://eddy.internal.example.comcredentialsSecret: eddy-credentialsconfig:auth:github:enabled: trueclientID: Iv23liAbCdEfGhIjKlMnallowedOrganizations: [acme]# allowedTeams: [acme/platform, acme/sre] # optional: also require one of these teams# teamsAsGroups: true # the defaultlocal:enabled: truemode: breakglass # keep a hidden admin password login, or set enabled: false
$ helm upgrade --install eddy-hub oci://ghcr.io/idestis/charts/eddy-hub --version 1.0.1 \-n eddy -f hub-values.yaml
- Grant RBAC in each workload cluster to the new groups. See Access and RBAC.
Verify
- Open
publicURLin a private window. You should reach GitHub's Authorize page for your app, then land back in Eddy. - Open
/api/v1/me. The groups should includeeddy:authenticated,eddy:github:acmeand oneeddy:github:acme/<team>per team. - The hub's audit log has a
loginevent with"provider":"github".
To rotate the secret, generate a second client secret in GitHub, update the Kubernetes Secret, restart the hub (kubectl -n eddy rollout restart deploy/eddy-hub), then delete the old secret in GitHub.
Alternative: a GitHub OAuth App
- In the organization (or your account), open Settings → Developer settings → OAuth Apps → New OAuth App.
- Set the Application name, the Homepage URL (your
publicURL) and the Authorization callback URL (<publicURL>/auth/github/callback). Leave Enable Device Flow unchecked and click Register application. - Copy the Client ID, click Generate a new client secret and copy the secret.
- Eddy asks for the read-only scopes
read:user user:email read:org. If the organization restricts OAuth apps (Organization settings → Third-party access), an owner must approve the app. Until then GitHub hides the user's membership and Eddy denies sign-in. - Continue with the Kubernetes Secret and chart values above. They are the same.
| GitHub App | OAuth App | |
|---|---|---|
| Access | Two read-only permissions, only in organizations where it is installed | Three read-only scopes, across every organization the user approves |
| Organization approval | Installing the app is the approval | An owner must approve it if OAuth app access is restricted |
| User tokens | Expire after 8 hours | Do not expire |
Eddy uses the token only during the callback either way.
GitHub Enterprise Server
Create the app on your server the same way, and set baseURL. Eddy then uses <baseURL>/login/oauth/… for the browser flow and <baseURL>/api/v3 for the API. Set apiURL if yours differs.
config:auth:github:enabled: trueclientID: Iv23liAbCdEfGhIjKlMnbaseURL: https://github.example.comallowedOrganizations: [acme]
The hub must reach the server over HTTPS. If its certificate comes from a private CA, add the CA to the hub image or set SSL_CERT_FILE through extraEnv with a volume. If the server is not on port 443, add an egress rule under networkPolicy.egress.extra.
OpenID Connect
Each entry in config.auth.oidc is one provider with its own button, routes /auth/<id>/login and /auth/<id>/callback, and sessions named oidc:<id>. The client secret comes from credentialsSecret under OIDC_<ID>_CLIENT_SECRET (the id upper-cased, - becoming _), or the variable named by clientSecretEnv. Add it with the add-a-key command.
| Key | Meaning |
|---|---|
id | Required. Lower case letters, digits and - |
preset | google, okta, entra or dex: fills in defaults. Your settings win |
issuer, clientID | The issuer URL (https) and the client ID. Discovery is <issuer>/.well-known/openid-configuration |
usernameClaim, groupsClaim | The claims that become the Kubernetes user and groups. Groups must be in the ID token: Eddy does not call userinfo |
allowedDomains, allowedGroups | Optional gates on sign-in |
userPrefix | Put in front of the user name, for example okta: |
The hub starts even if an issuer cannot be reached. Sign-in through that provider shows “cannot be reached” until discovery succeeds.
- In the Google Cloud console, pick a project. Under Google Auth Platform → Branding, set up the consent screen. With Google Workspace choose Audience: Internal.
- Under Clients → Create client, choose Web application and add the authorized redirect URI
https://eddy.internal.example.com/auth/google/callback. - Store the secret as
OIDC_GOOGLE_CLIENT_SECRETineddy-credentials, and add the values:config:auth:oidc:- id: googlepreset: googleclientID: 1234567890-abc.apps.googleusercontent.comallowedDomains: [example.com]groups:static: # Google ID tokens carry no groupsalice@example.com: [platform]bob@example.com: [platform, oncall]
allowedDomains is checked against the hd claim, which only Google Workspace accounts have, so a personal Gmail account is denied. For real group membership, put Dex in front of Google with its Google connector.
Okta
- In the Okta admin console: Applications → Create App Integration → OIDC → Web Application. Set the sign-in redirect URI to
https://eddy.internal.example.com/auth/okta/callbackand choose who is assigned. Copy the client ID and the secret (OIDC_OKTA_CLIENT_SECRET). - Put groups in the ID token. On the org authorization server, edit the app's Sign On → OpenID Connect ID Token: Groups claim type Filter, name
groups, matching a regex such as^eddy-. On a custom authorization server, add agroupsclaim of value type Groups included in the ID token, always. - Add the values:
config:auth:oidc:- id: oktapreset: oktaissuer: https://example.okta.com/oauth2/defaultclientID: 0oa1b2c3d4e5f6g7h8i9allowedGroups: [eddy-users] # optional gate; becomes eddy:eddy-users
Microsoft Entra ID
- In the Entra admin center: App registrations → New registration, single tenant. Add a Web redirect URI
https://eddy.internal.example.com/auth/entra/callback. - Certificates & secrets → New client secret. Copy the value (not the secret ID) into
OIDC_ENTRA_CLIENT_SECRET, and note when it expires. - Token configuration → Add groups claim. Choose Security groups, and Group ID for the ID token. Groups then arrive as object IDs, so you bind
eddy:<object id>in RBAC. - Use the tenant-specific issuer. The multi-tenant
commonandorganizationsendpoints do not work: the issuer in their discovery document does not match, and sign-in shows “cannot be reached”.config:auth:oidc:- id: entrapreset: entraname: Microsoftissuer: https://login.microsoftonline.com/00000000-0000-0000-0000-000000000000/v2.0clientID: 11111111-2222-3333-4444-555555555555
The user name is preferred_username (the UPN). Users in more than 200 groups hit Entra's overage: the claim is left out and Eddy sees no groups. Use groups assigned to the application, or app roles with groupsClaim: roles.
Dex
Dex federates LDAP, SAML, GitHub, Google groups and more behind one OIDC issuer. Add Eddy as a static client in the Dex config, then the provider in the hub values:
# Dex configstaticClients:- id: eddyname: EddysecretEnv: EDDY_CLIENT_SECRETredirectURIs: ["https://eddy.internal.example.com/auth/dex/callback"]
# hub-values.yamlconfig:auth:oidc:- id: dexpreset: dex # scopes include groupsissuer: https://dex.example.comclientID: eddy
Store Dex's secret for Eddy as OIDC_DEX_CLIENT_SECRET. Dex's connectors decide what groups holds. The GitHub connector emits org:team, which becomes eddy:acme:platform.
Group mapping
Every method goes through the same mapping. Its output is exactly what the agents impersonate.
- User: GitHub: the verified email or
github:<login>. OIDC: theusernameClaim. Proxy: the user header. Local users:local:<username>.system:users and anything matchingauth.denyUserPrefixes(defaultsystem:,eks:,kubernetes-admin) are refused. - Groups: every group gets the prefix
eddy:(auth.groups.prefix), andsystem:*groups are dropped. The prefix must be in the agent'simpersonation.allowedGroupPrefixes. - Every user also gets
eddy:authenticated, plus anygroups.staticentries for their email, login or user name.
Groups are captured at sign-in, so a team change takes effect at the next sign-in. Sessions end after 8 hours idle or 24 hours in total. To apply a change at once, sign out and in, or an admin can end every session and token of a user with eddy-hub admin revoke --user <user>. Then bind the groups in RBAC: Access and RBAC.
Local users
Local users keep working next to providers. Their hashes are argon2id (bcrypt at cost 12 or more is also accepted).
- Generate a hash. The command reads the password from standard input:
$ read -rs PW && printf '%s' "$PW" | docker run --rm -i ghcr.io/idestis/eddy-hub:1.0.1 hash-password - Put it in the values. A group
platformbecomes the Kubernetes groupeddy:platform:Or pointusers:list:- username: adminpasswordHash: "$argon2id$v=19$m=65536,t=3,p=4$...$..."groups: [platform]
users.existingSecretat a Secret with ausers.yamlkey.
Break-glass mode
mode: breakglass keeps password sign-in for emergencies such as an identity provider outage:
- The form is hidden. A small Admin sign-in link leads to
/login?local=1. - Logins are rate-limited like normal mode. Every attempt and success is logged as a
WARN, andloginaudit events carry"mode":"breakglass", so you can alert on them. - It needs another method (GitHub, OIDC or proxy), or the chart and hub refuse to start.
allowedUsers: [admin]limits password sign-in to those names.
config:auth:local: {enabled: true, mode: breakglass, allowedUsers: [admin]}
Set enabled: false to turn passwords off completely. A typical setup is one admin user in break-glass mode, with its password in a vault.
Trusted proxy (oauth2-proxy)
If you already run oauth2-proxy or Pomerium, Eddy can trust its headers. The chart can run oauth2-proxy as a sidecar: it listens on 4180 and forwards to the hub on 127.0.0.1:8080. The hub trusts identity headers only from 127.0.0.1/32 and only with a matching shared secret. /mcp bypasses the proxy, because MCP clients send only a personal access token. Do not add authentication in front of it.
- Add the shared secret, at least 32 random bytes, to
eddy-credentialsasEDDY_PROXY_SECRET(the add-a-key command with a generated value):$ kubectl -n eddy patch secret eddy-credentials --type merge \-p "{\"stringData\":{\"EDDY_PROXY_SECRET\":\"$(openssl rand -base64 48)\"}}"
- Create the proxy's own Secret:
$ kubectl -n eddy create secret generic eddy-oauth2-proxy \--from-literal=OAUTH2_PROXY_COOKIE_SECRET="$(openssl rand -base64 32 | tr -- '+/' '-_')" \--from-literal=OAUTH2_PROXY_CLIENT_ID=... \--from-literal=OAUTH2_PROXY_CLIENT_SECRET=...
- Add the values (Google shown):
credentialsSecret: eddy-credentials # holds EDDY_PROXY_SECRET; required with the sidecarconfig:auth:local:enabled: false # or keep it on as a break-glass logingroups:static:alice@example.com: [platform] # Google sends no groups, so map them hereoauth2Proxy:enabled: trueexistingSecret: eddy-oauth2-proxyextraArgs: ["--email-domain=example.com"] # who may sign inalphaConfig:providers:- id: googleprovider: googleclientID: ${OAUTH2_PROXY_CLIENT_ID}clientSecret: ${OAUTH2_PROXY_CLIENT_SECRET}
The chart enables proxy auth, sets trustedCIDRs to 127.0.0.1/32 and makes the proxy send X-Forwarded-Email, X-Forwarded-Groups and X-Eddy-Proxy-Secret. The oauth2-proxy alpha config can change between releases, so check a new version with oauth2-proxy --config-test. Tokens for proxy users last at most 30 days.
Private networks, VPNs and egress
GitHub and OIDC sign-in work for a hub that is only reachable on a private network. Both legs of the flow are browser redirects. The identity provider never connects to the hub, so a private callback such as https://eddy.internal.example.com is fine. The hub itself needs outbound HTTPS to the provider. The chart's default egress policy already allows TCP 443 anywhere, plus DNS.
| Provider | Hosts the hub calls |
|---|---|
| GitHub | github.com (token), api.github.com (user, emails, memberships, teams) |
| GitHub Enterprise Server | Your server: /login/oauth/access_token and /api/v3 |
accounts.google.com, oauth2.googleapis.com, www.googleapis.com | |
| Okta | <org>.okta.com |
| Entra ID | login.microsoftonline.com |
| Dex and others | The issuer host and whatever its discovery document names |
An outbound HTTP proxy is honoured through HTTPS_PROXY and NO_PROXY in extraEnv. Users' browsers also need to reach the provider's authorization page.
SAML
SAML 2.0 is not in v1.0. It is a possible future addition and would feed the same mapping. Like OIDC, it would work on private networks. Until then use OIDC (most SAML identity providers also speak it), Dex's SAML connector, or a trusted proxy.
Troubleshooting
| You see | Usual cause |
|---|---|
| The provider says the redirect URI does not match | The callback registered there differs from <publicURL>/auth/<id>/callback: check scheme, host, port and trailing slashes |
| “Your sign-in expired or was started in another tab” | More than 10 minutes at the provider, another tab, blocked cookies, or publicURL differs from the browser address |
| “Your account is not allowed” | Not an active member of an allowed organization, the GitHub App is not installed in it, the OAuth App is not approved, the email is unverified, or the domain or group is not allowed. The login audit event has the reason |
| “The sign-in provider returned an error” | Wrong or expired client secret, clock skew on the hub, or the token endpoint is unreachable. The hub log has a WARN with the cause, never the token |
| “cannot be reached” | OIDC discovery failed: check egress, DNS and the issuer URL |