SSO Login (OIDC)
honey kube login and honey ssh login can authenticate you with your
corporate single sign-on identity instead of an operator-issued enrollment
code: you sign in through a browser, honey verifies the resulting OIDC
id_token, an OPA policy maps your verified claims to a honey identity, and
honey issues a short-lived certificate — an mTLS client certificate for the
Kubernetes access proxy, an SSH certificate for the
SSH gateway — that the gateways already know how to
consume. No honey-specific credential is created or stored anywhere; your SSO
session is the only secret involved, and it never leaves your machine.
This is opt-in: with no oidc: block configured, the login endpoints don't
exist (404) and nothing about existing enrollment changes.
The two-policy model
Authorization here is split across two independent OPA decisions, each with
its own input.action:
identity— the role decision. It runs once, at login, and maps your verified SSO claims (email, groups, the raw token) to a Kubernetes user + groups and/or a set of SSH principals. This is the only place group/role mapping happens; the gateways never see your SSO claims.k8s_request(documented in full in Kubernetes Access Proxy) — the resource decision. It runs on every proxied Kubernetes API call and authorizes it by the identity baked into your certificate (never your original SSO claims, which the gateway never sees again after login).
The SSH gateway has no per-request resource policy of its own beyond certificate principal matching — see SSH Gateway for how principals map to inventory access there.
Both policies live in the same .rego package (HONEY_POLICY_DIR) as every
other honey decision — see Authorization.
Server configuration
Enable SSO login with an oidc: block:
# config.yaml
oidc:
issuer: https://your-oidc-provider.example/realms/corp # OIDC discovery issuer
client_id: honey-kube # expected token audience
scopes: ["groups"] # additional scopes; openid/email/groups are always requested
username_claim: email # claim mapped into the identity input as `email` (default: email)
groups_claim: groups # claim mapped into the identity input as `groups` (default: groups)
device_cert_ttl: 12h # validity of every device/SSO-issued certificate (default: 12h)
k8s_proxy:
clusters:
- name: prod
kubeconfig: /etc/honey/prod.kubeconfig
labels: # exposed to k8s_request policy as input.cluster_labels
env: prod
region: us-east
issuer and client_id are the only required fields — omit username_claim
/ groups_claim to use the common email / groups claim names your
provider likely already issues. scopes supplements, it doesn't replace,
openid email groups.
k8s_proxy.clusters[].labels are arbitrary key/value tags for a cluster,
independent of OIDC — they exist so a k8s_request policy can select by
attribute (environment, region, platform) instead of only by cluster name.
See the full k8s_proxy config reference.
device_cert_ttl and why it's short
honey has no certificate revocation: once issued, a certificate is valid
for its full lifetime no matter what happens to the identity behind it
(offboarding, a revoked SSO session, a compromised laptop). The mitigation is
keeping that lifetime short. device_cert_ttl defaults to 12h and governs
every certificate honey mints for a human — SSO-issued Kubernetes client
certs, SSO-issued SSH certs, and enrollment-code certs alike. Lower it if your
threat model calls for it; there is no floor enforced, only sane defaults.
The identity policy
At login, honey evaluates:
{
"action": "identity",
"target": "kube", // or "ssh"
"cluster": "prod", // requested cluster name; "" for ssh
"subject": "user-abc123", // the token's sub claim
"email": "alice@corp.example",
"groups": ["eng", "on-call"],
"claims": { "...": "the full decoded id_token claim set" }
}
and expects an identity object back:
package honey
import rego.v1
default allow := false
# Map the "eng" SSO group to a honey identity: a Kubernetes user + group, and
# an SSH principal set. `input.email` becomes the certificate CN either way.
identity := {
"user": input.email,
"groups": ["developers"],
"principals": ["ubuntu", input.email],
} if {
input.action == "identity"
"eng" in input.groups
}
allow if {
input.action == "identity"
identity
}
This is fail-closed: a subject with no identity object, or where allow
is false, is denied login outright (403) — no certificate is issued. There
is no default identity mapping; without a policy that sets identity, SSO
login always fails.
identity.user becomes the certificate's CN (the Kubernetes impersonated
user / the SSH certificate KeyId); identity.groups becomes the client
certificate's O= (Subject Organization) fields, which is what
k8s_request policies see as input.groups; identity.principals becomes
the SSH certificate's valid principals. Groups and principals are
honey-CA-attested: they are read back out of the certificate honey itself
signed, never asserted by the client at connection time.
The identity policy also receives input.cluster (the cluster named at
honey kube login <cluster>), but the issued certificate is not
cluster-scoped — it carries only the user and groups. Use input.cluster to
decide which identity to grant, not to fence access to a single cluster;
per-cluster and per-resource access is enforced on every request by the
k8s_request policy below (via input.cluster, input.cluster_labels, and the
request fields), which is the authoritative gate.
The k8s_request policy: fine-grained resource authorization
Once a Kubernetes identity is issued, every proxied API call is a separate
k8s_request decision (see Kubernetes Access
Proxy for the full input shape). Combining
group, cluster label, namespace, resource, verb, and a name regex gives you a
fine-grained, resource-level access role — for example, read-only cluster-wide
access plus a narrower exec grant into a specific namespace's dev pods:
package honey
import rego.v1
default allow := false
default deny_reason := "denied by policy"
# developers: read-only on every "staging"-labelled cluster
allow if {
input.action == "k8s_request"
"developers" in input.groups
input.cluster_labels.env == "staging"
input.verb in {"get", "list", "watch"}
}
# developers: exec only into pods named "dev-*" in the "sandbox" namespace,
# on the same staging clusters
allow if {
input.action == "k8s_request"
"developers" in input.groups
input.cluster_labels.env == "staging"
input.namespace == "sandbox"
input.resource == "pods"
input.subresource == "exec"
regex.match(`^dev-`, input.name)
}
Because input.groups here comes from the certificate's O= fields (set by
the identity policy at login, not by the caller), a k8s_request policy
never needs to trust anything the client sends on the wire — the same
enforcement holds whether the identity came from SSO or an enrollment code.
User commands
honey kube login
# SSO branch (default): omit --enroll-code
honey kube login prod --proxy honey-host:6443
# enrollment-code branch: unchanged, still available for CI/headless use
honey kube login prod --enroll-code abc123 \
--proxy honey-host:6443 --proxy-ca serving-ca.pem
Without --enroll-code, honey kube login <cluster> opens your browser for
an OIDC sign-in, exchanges the verified identity for a signed client
certificate, and writes a honey-<cluster> kubeconfig context — same as the
enrollment-code path, but the server also returns its own serving CA in the
response so --proxy-ca / --insecure-skip-tls-verify are usually
unnecessary (they remain available as an override, and as mutually-exclusive
alternatives if the server doesn't know its own CA). --admin-url selects the
honey web instance the browser flow talks to (default $HONEY_WEB_URL, else
http://localhost:8765); --proxy is always required — it's the address
kubectl connects to, independent of where the login itself happens.
kubectl --context honey-prod get pods
Auto-refreshing kube login (kubectl exec credential plugin)
The SSO branch of honey kube login <cluster> writes a kubeconfig user
entry that is a kubectl exec credential plugin, not an embedded
certificate: instead of storing client-certificate-data /
client-key-data, it stores a command (honey kube login <cluster> --admin-url <url>) that kubectl runs on demand. kubectl calls this command
before every request that needs credentials, so honey transparently
re-authenticates through your OIDC provider whenever the cached certificate
has actually expired — the browser opens only on that expiry path, never
on every call, and kubectl never surfaces an expired-certificate error. You
never manually re-run honey kube login because it expired; kubectl does
it for you the next time you use the context. During interactive sign-in
(setup or a triggered refresh) the browser now opens automatically; the
sign-in URL is still printed to stderr as a fallback for when a browser
can't be opened.
honey kube login prod --proxy honey-host:6443
kubectl --context honey-prod get pods # works today...
# ...and still works after the certificate expires: kubectl silently re-runs
# "honey kube login prod --admin-url ..." behind the scenes, honey notices
# the cached cert is stale, opens the browser for a fresh sign-in, and
# hands kubectl a new certificate — no re-login command, no error.
kubectl --context honey-prod get pods
Prerequisite: honey must be on the PATH kubectl uses. kubectl
launches the exec plugin as a subprocess by the bare command name (honey),
resolved through its own environment's PATH — not necessarily an
interactive shell's. If honey isn't reachable there, every request fails
with a generic exec-plugin error. Confirm it with which honey in the same
environment kubectl runs in, or use --static below instead.
--static opts out. honey kube login prod --proxy ... --static embeds
the certificate and key directly in the kubeconfig, reproducing the
pre-auto-refresh behavior: no exec plugin, no re-authentication, and no
honey PATH requirement at kubectl-call time. Use it on hosts where honey
won't be installed alongside kubectl (e.g. a kubeconfig handed to a CI
runner or another machine). Because there's no cached SSO session to refresh
from, you re-run honey kube login by hand once the certificate expires.
The --enroll-code path is always static, regardless of --static — an
enrollment code has no SSO session to refresh from either.
--no-browser prints the sign-in URL to stderr instead of attempting to
open a browser, for headless hosts and SSH sessions — the same flag works
whether the sign-in is the initial honey kube login or a refresh triggered
by the exec plugin.
Credential cache. A successful sign-in caches the issued certificate and
key under ~/.honey/kube/<cluster>/ (honors $HONEY_HOME), with the files
written 0600. This cache is what lets the exec plugin serve most requests
with no network round trip at all — it only re-authenticates when the cached
certificate is within a minute of NotAfter. The exec-plugin kubeconfig
entry itself carries no certificate or key material — just the command used
to fetch them — so a leaked kubeconfig alone can't be used as a credential.
Non-interactive kubectl (CI, interactiveMode: Never). Some kubectl
invocations tell exec plugins they may not prompt or open a browser. If the
cached certificate has expired in that context, honey fails closed with an
error instead of hanging on a browser that will never be seen — refresh the
cache first by running honey kube login <cluster> interactively (with a
real browser or --no-browser) from an environment that allows it.
Composes with brokered intercept. Brokered honey intercept --cluster <cluster> (see Local Interception) resolves the same
honey-<cluster> kubeconfig context that honey kube login writes, so its
port-forward inherits this auto-refresh for free — no separate credential
path to keep alive for intercept.
honey ssh login
honey ssh login --admin-url https://honey.example
Runs the same browser OIDC sign-in, then exchanges the identity for a signed
SSH user certificate. --identity selects the private key to certify
(default ~/.ssh/honey_ed25519; generated on first use if it doesn't exist);
--out overrides where the certificate is written (default:
<identity>-cert.pub, which OpenSSH loads automatically alongside the key).
ssh -i ~/.ssh/honey_ed25519 alice@gateway-host -p 12222 <resource>
Security model
- PKCE (S256),
state, and a loopback-only redirect. The CLI generates a fresh PKCE verifier/challenge and a randomstateper login, listens on127.0.0.1:0, and only accepts the authorization code back on that loopback address — there is no shared client secret and no network-reachable redirect target. - A caller-bound
nonce(defense-in-depth, not a replay store). The CLI generates a randomnonce, sends it with the authorization request, and honey re-verifies it against theid_token'snonceclaim — binding the token to the caller's own login request. honey stores no nonce server-side, so this is not a replay guard for a leaked, still-valid token; short token lifetimes (the provider controls theid_tokenTTL) and TLS transport are the mitigations there. Theid_tokenis a bearer credential — treat it as a short-lived secret. - Full
id_tokenverification, fail-closed. honey verifies the token's signature against the issuer's published keys, and checksiss,aud(must equalclient_id), andexp— analg: nonetoken, an expired token, a wrong-audience token, or a nonce mismatch are all rejected outright. Any verification or policy-evaluation error denies the login; nothing defaults to allow. - Groups and principals are honey-CA-attested, never client-asserted.
The
identitypolicy's output is baked into the certificate at issuance (O=for Kubernetes groups, valid principals for SSH); the gateways read those fields back out of the certificate they verified, so a client cannot claim a different group or principal after login by altering a request. - No secrets logged. The
id_token, certificates, CSRs, and public keys are never written to the audit log or application logs — only the action, target, resolved actor, and allow/deny decision (kube_login/ssh_loginaudit events). - Short-lived certificates, no revocation. See
device_cert_ttlabove — this is the primary mitigation for a compromised SSO session or an offboarded user whose certificate is still technically valid.