- Go 87.5%
- Makefile 5.8%
- Shell 5.1%
- Dockerfile 1.6%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| api/v1alpha1 | ||
| cmd | ||
| config | ||
| docs | ||
| hack | ||
| internal/controller | ||
| test | ||
| .gitattributes | ||
| .gitignore | ||
| AGENTS.md | ||
| BUGFIXES.md | ||
| CHANGELOG.md | ||
| ci-test.Dockerfile | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| Makefile | ||
| operator.mk | ||
| PROJECT | ||
| QUICKSTART.md | ||
| README.md | ||
| setup.sh | ||
| VERSION | ||
kratos-identity-operator
This is a Kubernetes operator. It manages Ory Kratos identities declaratively.
You describe a user as a KratosIdentity custom resource. The description includes traits, group memberships, optional POSIX attributes, and the Secret that holds the password.
The operator reconciles this desired state against a Kratos instance over its Admin API. It creates, updates, and deletes identities to match the CR. It re-pushes the identity on password rotation.
This operator is one of the seanfarm operator fleet. The fleet also includes forgejo-, minio-resource-, and openbao-operators. These operators share the same reconcile harness.
Custom resources
| Kind | API group | Purpose |
|---|---|---|
KratosIdentity |
identity.kratos.sean.farm/v1alpha1 |
A single Kratos identity. Holds the target Admin API URL. Holds traits: username, email, display name, type, groups, and an optional POSIX block. Holds the Kratos schema ID and a reference to the Secret key that carries the password. Status holds 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 of these are separate CRD kinds.
The CRD enforces the input shape at the API server. For example, this covers the email pattern, and group and length bounds.
The kratosAdminURL field is immutable. A CEL self == oldSelf rule
enforces this. The field is also bound at runtime 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. This includes finalizer mechanics, external-client resolution, and mapping of a handler's convergence result onto the Ready condition. It also includes a reason-to-requeue taxonomy, deletion-policy semantics, and conflict-safe status writes. It also includes secret-watch mapping, bounded and sanitized status messages, and uniform concurrency with exponential backoff.
The generic entry point is reconcile.Run[T, C]. This operator supplies only
the parts that are Kratos-specific.
Ready is structural. Only the harness sets Ready=true. The handler never
sets it. The harness sets Ready=true only after Converge reports that it
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 state
is not yet observable. This rule makes sure the operator never asserts Ready
ahead of reality.
The two pieces of policy this operator provides:
-
Handler (
Converge+Delete). This is a direct-fit handler. It needs no adapter.-
Convergereads the password Secret. It then makes sure the identity exists in Kratos and matches the spec. It creates the identity if absent. Otherwise it updates traits and password in a single PUT.ConvergereturnsConvergedonly after a read-back GET confirms the identity is observable. This is the structural fix for the old "Ready the instant create returned" bug.Convergealso detects out-of-band drift, for example an identity deleted in Kratos, during the steady-state re-verify, and re-provisions it. It re-adopts an identity whose prior status write was lost, counted as an orphaned recovery, instead of creating a duplicate. -
Deleteremoves the identity from Kratos. It recovers the ID by email if status lost it. Deletion uses hold until success. A transient Kratos failure keeps the finalizer in place and retries. This rule makes sure that deleting the CR cannot leave a live login credential behind with no Kubernetes object left to manage it. -
Set
spec.deletionPolicy: Retainbefore a management handoff. Deleting that CR releases its finalizer without a Kratos request or a password Secret read. The identity, credentials, and sessions remain in Kratos. Retain also works when the saved identity ID is absent or Kratos is unavailable. The defaultDeletepolicy preserves existing behavior.spec: deletionPolicy: RetainApply the policy change before removing the CR from its source. With Flux
prune: false, remove the retained live CR explicitly after the source change is applied. Verify the account by its saved Kratos ID.
-
-
Taxonomy. A small table classifies each failure reason.
InvalidSpecis terminal. This means it is a spec-level problem, and the watch re-triggers on the next edit. Every other converge error falls through to the default backoff disposition. It then returns to controller-runtime for exponential retry, using the library's shared rate limiter.
Notable wiring:
- External client.
Resolvebuilds a token-lesskratosAdminClient. This client is a shared*http.Clientplus the namespace-boundspec.kratosAdminURL. All Kratos API-call metrics live on this client, so they fire regardless of the caller. ObservedGenerationis bumped only onConverged. This is theObservedGenOnConvergedOnlypolicy. A not-yet-converged generation stays "unobserved". This rule stops the handler's generation-skip fast path from short-circuiting the Progressing loop before the remote is verified.- Secret-rotation fast path.
status.observedSecretResourceVersiongives the generation-skip fast path an extra guard. A password rotation does not change the CR generation. Without this guard, a rotated password would never reach Kratos. Recording the Secret'sresourceVersionmakes a Secret-only change fall through to the PUT. The handler still does a cheap verify-GET in this case. - Secret watch. This is delegated to the library's
MapSecretToRequests. The only Kratos-specific part is the per-type ref accessor. A Secret create or update enqueues everyKratosIdentityin that namespace that references it. This means a reconcile that ran before the Secret existed retries immediately, instead of waiting out a requeue interval. - Concurrency and backoff. This operator is the canonical adopter of the
library's uniform concurrency-and-backoff knob,
ControllerTuning.MaxConcurrentReconcilesis kept modest on purpose, because a single shared Kratos Admin API sits behind every CR. The exponential backoff schedule uses the shared library default. - Status mirrors and metrics. The harness
MirrorStatusandOnStatusWritehooks keep the legacystatus.readybool andstatus.phasestring as harness-maintained mirrors of the Conditions. The Conditions remain the source of truth. The same hooks drive the Prometheus phase gauge and the reconcile and status-update metrics.
Security
Condition reasons, condition 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 and Event messages are capped. The library
bounds every message it writes to a fixed byte limit, on a UTF-8 rune
boundary. This operator also caps any non-2xx Kratos response body before it
can be placed into an error string. The cap serves two purposes. It is an
availability backstop, because of etcd object-size limits. It is also a
disclosure backstop. The cap only bounds the message. It does not sanitize
the message by itself. The library also offers a
SanitizingRecorderfor Event sites. The real protection is the use of generic messages at credential-bearing sites. - Name the Secret, never quote it. A missing-password path emits
SecretNotFoundand names the Secret. It never echoes the password. No plaintext password ever reaches status, Events, or logs. - No password material on status.
status.observedSecretResourceVersiondeliberately holds the Secret's opaqueresourceVersionetag, not a hash of the password. A password hash on a world-readable status would be material an attacker could attack offline. 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 targets and cross-namespace targets asInvalidSpec. The field is also immutable, enforced by the CEL ruleself == oldSelf. The Kratos Admin API is unauthenticated. These checks stop 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 changes Secrets.
Build & distribution
- Images are built in-cluster. Native Binjovi tasks run the tests and build AMD64 and ARM64 candidates with BuildKit. Release publishes the exact candidate digests and verifies their cosign signatures. Local image builds are not the cluster delivery path.
- Forgejo is canonical. Codeberg is the backup. Day-to-day pushes go to
Forgejo, at
code.sean.farm. AForgejoPushMirrorbacks each repo up tocodeberg.org/someara/<repo>on a short interval.make rebuildre-seeds Forgejo from codeberg. Go module fetches resolvecodeberg.org/someara/...through the default GOPROXY. - CI uses native Binjovi agents. This project does not use Argo Workflows, Tekton, Shipwright, or repository-hosted Actions for delivery.
Development
-
Red-green TDD. Tests drive behavior first. The controller package carries envtest and Ginkgo suites, plus focused regression tests for the harness adoption, the generation-skip fast path, password rotation, and the secret watch.
-
Standard targets (from the Makefile):
Target What it does make manifestsRegenerate CRDs and RBAC from kubebuilder markers make generateRegenerate DeepCopymethodsmake fmt/make vetgo fmtandgo vetmake testRun the unit and envtest suite with coverage make test-e2eRun e2e tests against a cluster make lint/make lint-fixRun golangci-lint. make lint-fixalso passes--fixmake build/make runBuild the manager binary, and run it against your kubeconfig make install/make uninstallApply or remove the CRDs make deploy/make undeployApply or remove the controller manifests -
How a change ships. Edit the code. Test it. Push it to Forgejo. A native Binjovi Build and Release then produce a cosign-signed image. The deployment is rolled by bumping the image pin in the Flux or Crossplane composition that references this operator. That composition lives in the platform repos, not here. Module and version references to the shared library are managed the same way.
Binjovi Build and Release proof
Binjovi tests each pull request in an isolated envtest task. It then builds one AMD64 and ARM64 OCI candidate from the exact revision. Binjovi is now the guarded Build and Release authority. Pipelines no longer declares this project. This revision also proves immediate signed webhook delivery and schedulable image execution under shared cluster load.
License
Apache-2.0. See the source-file headers.
Native CI
Binjovi builds pull requests with the recipe in
sean/binjovi-plans:recipes/kratos-identity-operator/. The active plan fixes the recipe
revision for each build. Recipe changes use a plans pull request and release;
they do not require a Binjovi runtime deployment.
The native Binjovi result is the only admission result for this project.
Task timing and release verification
Open the project dashboard and expand an execution attempt for the native task timeline and available CPU, off-CPU, and syscall profiles. Release is this project's pipeline endpoint; there is no configured Deploy target. Consumer image pins are separate changes.
Use binjovictl release kratos-identity-operator --build BUILD_ID --wait
for an exact successful Build. Confirm the published image digest and signature
in Release evidence. A task profile explains execution; it does not replace
artifact verification.