No description
  • Go 87.5%
  • Makefile 5.8%
  • Shell 5.1%
  • Dockerfile 1.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-09 18:10:39 +02:00
api/v1alpha1 feat: retain Kratos identities during management handoff 2026-09-06 04:16:43 +02:00
cmd fix(security): refuse redirects on Kratos Admin API calls 2026-07-27 20:19:50 +02:00
config feat: retain Kratos identities during management handoff 2026-09-06 04:16:43 +02:00
docs docs: rewrite documentation to ASD-STE100 2026-08-07 18:44:11 +02:00
hack docs: complete Phase 6 - API stability and operational tooling 2026-04-24 10:30:57 +02:00
internal/controller feat: retain Kratos identities during management handoff 2026-09-06 04:16:43 +02:00
test docs: rewrite documentation to ASD-STE100 2026-08-07 18:44:11 +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
AGENTS.md docs: rewrite documentation to ASD-STE100 2026-08-07 18:44:11 +02:00
BUGFIXES.md docs: rewrite documentation to ASD-STE100 2026-08-07 18:44:11 +02:00
CHANGELOG.md fix(reconcile): requeue ConfigurationError instead of dropping the identity 2026-07-28 16:04:31 +02:00
ci-test.Dockerfile build: align validation toolchain and Athens fallback 2026-07-24 10:58:46 +02:00
Dockerfile perf(kratos): namespace BuildKit Go caches 2026-09-06 00:18:29 +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 docs: rewrite documentation to ASD-STE100 2026-08-07 18:44:11 +02:00
README.md docs: explain native delivery and task telemetry 2026-09-09 18:10:39 +02:00
setup.sh Initial kratos-identity-operator implementation 2026-04-24 01:02:53 +02:00
VERSION chore: devbump 0.5.17 -> 0.5.18-dev 2026-07-24 09:40:11 +00:00

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.

    • Converge reads 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. Converge returns Converged only after a read-back GET confirms the identity is observable. This is the structural fix for the old "Ready the instant create returned" bug. Converge also 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.

    • Delete removes 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: Retain before 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 default Delete policy preserves existing behavior.

      spec:
        deletionPolicy: Retain
      

      Apply 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. InvalidSpec is 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. Resolve builds a token-less kratosAdminClient. This client is a shared *http.Client plus the namespace-bound spec.kratosAdminURL. All Kratos API-call metrics live on this client, so they fire regardless of the caller.
  • ObservedGeneration is bumped only on Converged. This is the ObservedGenOnConvergedOnly policy. 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.observedSecretResourceVersion gives 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's resourceVersion makes 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 every KratosIdentity in 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. MaxConcurrentReconciles is 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 MirrorStatus and OnStatusWrite hooks keep the legacy status.ready bool and status.phase string 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 SanitizingRecorder for 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 SecretNotFound and names the Secret. It never echoes the password. No plaintext password ever reaches status, Events, or logs.
  • No password material on status. status.observedSecretResourceVersion deliberately holds the Secret's opaque resourceVersion etag, 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 as InvalidSpec. The field is also immutable, enforced by the CEL rule self == 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. A ForgejoPushMirror backs each repo up to codeberg.org/someara/<repo> on a short interval. make rebuild re-seeds Forgejo from codeberg. Go module fetches resolve codeberg.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 manifests Regenerate CRDs and RBAC from kubebuilder markers
    make generate Regenerate DeepCopy methods
    make fmt / make vet go fmt and go vet
    make test Run the unit and envtest suite with coverage
    make test-e2e Run e2e tests against a cluster
    make lint / make lint-fix Run golangci-lint. make lint-fix also passes --fix
    make build / make run Build the manager binary, and run it against your kubeconfig
    make install / make uninstall Apply or remove the CRDs
    make deploy / make undeploy Apply 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.