Kubernetes operator for managing Ory Kratos identities
  • Go 86.7%
  • Makefile 6.2%
  • Shell 5.4%
  • Dockerfile 1.7%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
pipeline-release-prepare cb6f0c220b
All checks were successful
pipeline/ci CI green v0.5.17
release: v0.5.17 (auto-release)
2026-07-24 09:25:51 +00:00
api/v1alpha1 fix(controller): constrain Kratos admin requests 2026-07-23 19:14:19 +02:00
cmd feat(validation): add Tier 1 CRD and configuration validation 2026-04-24 11:36:54 +02:00
config fix(controller): constrain Kratos admin requests 2026-07-23 19:14:19 +02:00
docs fix(controller): constrain Kratos admin requests 2026-07-23 19:14:19 +02:00
hack docs: complete Phase 6 - API stability and operational tooling 2026-04-24 10:30:57 +02:00
internal/controller fix(identity): repair remote drift and classify API errors 2026-07-24 10:46:28 +02:00
test fix(identity): repair remote drift and classify API errors 2026-07-24 10:46:28 +02:00
.gitattributes fix(cicd): auto-resolve CHANGELOG land conflicts via .gitattributes merge=union 2026-07-05 12:14:39 +00:00
.gitignore refactor: migrate KratosIdentity onto a shared reconcile harness 2026-06-09 15:12:31 +02:00
BUGFIXES.md fix(identity): repair remote drift and classify API errors 2026-07-24 10:46:28 +02:00
CHANGELOG.md release: v0.5.17 (auto-release) 2026-07-24 09:25:51 +00:00
ci-test.Dockerfile build: align validation toolchain and Athens fallback 2026-07-24 10:58:46 +02:00
Dockerfile build: align validation toolchain and Athens fallback 2026-07-24 10:58:46 +02:00
go.mod fix(identity): repair remote drift and classify API errors 2026-07-24 10:46:28 +02:00
go.sum fix(identity): repair remote drift and classify API errors 2026-07-24 10:46:28 +02:00
Makefile chore(scaffolding): adopt operator.mk + httpkit helpers (lib v0.4.12) 2026-07-17 12:29:22 +02:00
operator.mk chore(scaffolding): adopt operator.mk + httpkit helpers (lib v0.4.12) 2026-07-17 12:29:22 +02:00
PROJECT Initial kratos-identity-operator implementation 2026-04-24 01:02:53 +02:00
QUICKSTART.md Initial kratos-identity-operator implementation 2026-04-24 01:02:53 +02:00
README.md fix(controller): constrain Kratos admin requests 2026-07-23 19:14:19 +02:00
setup.sh Initial kratos-identity-operator implementation 2026-04-24 01:02:53 +02:00
VERSION release: v0.5.17 (auto-release) 2026-07-24 09:25:51 +00:00

kratos-identity-operator

A Kubernetes operator that manages Ory Kratos identities declaratively. You describe a user (traits, group memberships, optional POSIX attributes, and the Secret holding their password) as a KratosIdentity custom resource; the operator reconciles that desired state against a Kratos instance over its Admin API — creating, updating, and deleting identities to match the CR, and re-pushing on password rotation. It is one of the seanfarm operator fleet (alongside forgejo-, minio-resource-, and openbao-operators) and shares their reconcile harness.

Custom resources

Kind API group Purpose
KratosIdentity identity.kratos.sean.farm/v1alpha1 A single Kratos identity. Holds the target Admin API URL, traits (username, email, display name, type, groups, optional POSIX block), the Kratos schema ID, and a reference to the Secret key carrying the password. Status carries the Kratos-assigned identity UUID, a Ready condition, and a phase mirror.

KratosIdentityList is the list type; SecretKeyRef and PosixAttributes are embedded spec structs — none are separate CRD kinds. The CRD enforces input shape at the API server (e.g. email pattern, group/length bounds), and kratosAdminURL is immutable via a CEL self == oldSelf rule and runtime-bound to the Kratos Admin Service in the CR's namespace (see Security).

Architecture

This operator is a thin policy shim over the shared reconcile harness in libseanfarm-operator/reconcile. The harness owns the entire reconcile lifecycle — finalizer mechanics, external-client resolution, mapping a handler's convergence result onto the Ready condition, a reason→requeue taxonomy, deletion-policy semantics, conflict-safe status writes, secret-watch mapping, bounded/sanitized status messages, and uniform concurrency with exponential backoff. The generic entrypoint is reconcile.Run[T, C]; this operator supplies only the parts that are Kratos-specific.

Ready is structural. The harness — never the handler — sets Ready=true, and only after Converge reports it actually verified the external state. The handler reports an Outcome of Converged or Progressing; an error maps to Degraded. The handler must return Progressing (not Converged) while the remote is not yet observable, so Ready can never be asserted ahead of reality.

The two pieces of policy this operator provides:

  • Handler (Converge + Delete). A direct-fit handler — no adapter needed.

    • Converge reads the password Secret, then ensures the identity exists in Kratos and matches the spec (create if absent, otherwise update traits + password in a single PUT). It returns Converged only after a read-back GET confirms the identity is observable — the structural fix for the old "Ready the instant create returned" bug. It also detects out-of-band drift (an identity deleted in Kratos) on the steady-state re-verify and re-provisions, and re-adopts an identity whose prior status write was lost (counted as an orphaned recovery) rather than creating a duplicate.
    • Delete removes the identity from Kratos, recovering the ID by email if status lost it. Deletion uses hold until success: a transient Kratos failure retains the finalizer and retries, so deleting the CR cannot leave a live login credential behind with no Kubernetes object left to manage it.
  • Taxonomy. A small table classifying each failure reason. InvalidSpec is terminal (a spec-level problem; the watch re-triggers on the next edit); every other converge error falls through to the default backoff disposition and returns to controller-runtime for exponential retry (the lib's shared rate limiter).

Notable wiring:

  • External client. Resolve builds a token-less kratosAdminClient — a shared *http.Client plus the namespace-bound spec.kratosAdminURL. All Kratos API-call metrics live on this client so they fire regardless of caller.
  • ObservedGeneration is bumped only on Converged (the ObservedGenOnConvergedOnly policy). A not-yet-converged generation stays "unobserved" so the handler's generation-skip fast path can't short-circuit the Progressing loop before the remote is verified.
  • Secret-rotation fast path. The generation-skip fast path is additionally guarded by status.observedSecretResourceVersion. A password rotation does not change the CR generation, so without this guard a rotated password would never reach Kratos; recording the Secret's resourceVersion makes a Secret-only change fall through to the PUT, while still doing a cheap verify-GET.
  • Secret watch. Delegated to the lib's MapSecretToRequests; the only Kratos-specific part is the per-type ref accessor. A Secret create/update enqueues every KratosIdentity in that namespace referencing it, so a reconcile that ran before the Secret existed retries immediately instead of waiting out a requeue interval.
  • Concurrency + backoff. This is the canonical adopter of the lib's uniform concurrency/backoff knob (ControllerTuning). MaxConcurrentReconciles is kept modest on purpose — there is a single shared Kratos Admin API behind every CR — while the exponential backoff schedule is the shared lib default.
  • Status mirrors + metrics. Via the harness MirrorStatus / OnStatusWrite hooks, the legacy status.ready bool and status.phase string are kept as harness-maintained mirrors of the Conditions (the Conditions are the source of truth), and the same hooks drive the Prometheus phase gauge and reconcile/ status-update metrics.

Security

Condition reasons/messages and Events land on world-readable status surfaces (relative to Secrets) and are persisted to etcd. This operator follows the seanfarm credential-disclosure contract:

  • Bounded messages. Condition/Event messages are capped (the lib bounds every message it writes to a fixed byte limit, on a UTF-8 rune boundary); this operator additionally caps any non-2xx Kratos response body before it can be interpolated into an error string. The cap is an availability backstop (etcd object-size limits) and a disclosure backstop — it bounds, it does not by itself sanitize. The lib also offers a SanitizingRecorder for Event sites; the real protection is generic messages at credential-bearing sites.
  • Name the Secret, never quote it. A missing-password path emits SecretNotFound naming the Secret — it never echoes the password, and no plaintext password ever reaches status, Events, or logs.
  • No password material on status. status.observedSecretResourceVersion is deliberately the Secret's opaque resourceVersion etag, not a hash of the password. A password hash on a world-readable status would be offline-attackable material; an etag detects rotation without disclosing anything.
  • Namespace-bound Admin API target. The controller accepts only http://kratos-admin.<resource-namespace>.svc.cluster.local:4434; it rejects arbitrary and cross-namespace targets as InvalidSpec. The field is also immutable (CEL self == oldSelf). The Kratos Admin API is unauthenticated, so these checks prevent a CR author from redirecting the operator—and the password it submits—to an attacker endpoint.
  • Least-privilege RBAC on Secrets. The operator's Secret access is read-only (get;list;watch); it never writes or mutates Secrets.

Build & distribution

  • Images are built in-cluster. Operator container images are produced by Shipwright (buildkit strategy) inside the seanfarm cluster and cosign-signed — never built on a developer laptop. The scaffolded docker-build/docker-buildx Makefile targets exist but are not the shipping path.
  • Forgejo is canonical; codeberg is the backup. Day-to-day pushes go to Forgejo (code.sean.farm). A ForgejoPushMirror backs each repo up to codeberg.org/someara/<repo> on a short interval, and make rebuild re-seeds Forgejo from codeberg. Go module fetches resolve codeberg.org/someara/... through the default GOPROXY.
  • CI runs in-cluster. Tests, builds, and image bakes run via Argo Workflows / Tekton + Shipwright inside the cluster — not via Actions-style CI pipelines.

Development

  • Red/green TDD. Behavior is driven by tests first; the controller package carries envtest/Ginkgo suites plus focused regression tests for the harness adoption, the generation-skip fast path, password rotation, and secret-watch.

  • Standard targets (from the Makefile):

    Target What it does
    make manifests Regenerate CRDs + RBAC from kubebuilder markers
    make generate Regenerate DeepCopy methods
    make fmt / make vet go fmt / go vet
    make test Run the unit + envtest suite with coverage
    make test-e2e Run e2e tests against a cluster
    make lint / make lint-fix golangci-lint (with --fix for the latter)
    make build / make run Build the manager binary / run it against your kubeconfig
    make install / make uninstall Apply / remove the CRDs
    make deploy / make undeploy Apply / remove the controller manifests
  • How a change ships. Edit → test → push to Forgejo → an in-cluster Shipwright Build/BuildRun produces a cosign-signed image → the deployment is rolled by bumping the image pin in the Flux/Crossplane composition that references this operator (in the platform repos, not here). Module/version references to the shared library are managed the same way.

License

Apache-2.0 (per the source-file headers).