Skip to content

fix: skip init-data-owner in namespaces enforcing restricted Pod Security - #618

Open
stubbi wants to merge 28 commits into
mainfrom
fix/data-owner-pss-aware
Open

stubbi wants to merge 28 commits into
mainfrom
fix/data-owner-pss-aware

Conversation

@stubbi

@stubbi stubbi commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

Follow-up to #608 (which fixed #607), addressing the concern @jtatum raised there.

Problem

#608 added the init-data-owner init container, which runs as root with CAP_CHOWN and is on by default. A namespace that enforces the restricted Pod Security Standard rejects such a pod at admission. After an operator upgrade, the next rollout of an existing instance in such a namespace would delete the running pod and then fail to create its replacement.

Change

spec.storage.fixOwnership becomes tri-state:

Value Behavior
unset (default) Run init-data-owner, except in namespaces labelled pod-security.kubernetes.io/enforce: restricted
true Always run it
false Never run it
  • The +kubebuilder:default=true marker is removed so that unset is distinguishable from an explicit true. fix: chown the data volume root to the pod UID before OpenClaw starts (#607) #608 has not been released, so no stored objects depend on the default.
  • The controller reads the instance's Namespace with an uncached reader and resolves the default in memory only, the same way cluster defaults are applied. The stored spec is never written back, so relabelling the namespace takes effect on the next reconcile.
  • New RBAC rule: get on namespaces (kubebuilder marker, generated role, Helm chart rules, OLM bundle). It also works in watchNamespaces mode, because a namespaced Role can grant get on its own Namespace object.
  • If the namespace cannot be read while fixOwnership is unset (for example RBAC from an older install), the controller does not guess. Reconciliation fails before any managed resource is touched, with a ReconcileFailed event and Ready condition that name the missing get on namespaces, and is retried. The existing StatefulSet stays unchanged. An explicit true/false skips the lookup.
  • Regenerates the CRDs and docs/api-reference.md. The latter also fixes the API Docs Sync check that fix: chown the data volume root to the pod UID before OpenClaw starts (#607) #608 left failing on main.

Testing

  • make test passes locally (unit + envtest).
  • New envtest suite internal/controller/data_owner_pod_security_test.go: default in an unlabelled namespace, baseline, restricted, explicit true and false, that the stored spec stays unset, that a Namespace-read failure returns an error and leaves a restricted-compatible StatefulSet unchanged, and that explicit values skip the lookup.
  • Unit test for the label helper in internal/resources/data_owner_test.go.
  • hack/check-helm-rbac-sync.sh passes. I could not run hack/check-helm-cluster-rbac.sh locally (no helm), so that one is covered by CI only.
  • Not exercised on a real cluster with Pod Security Admission enforcing; envtest does not create pods.

Not covered

Namespaces exempted from Pod Security Admission through the admission configuration still carry the label, so the init container is skipped there by default. Set fixOwnership: true in that case. Other policy engines (Kyverno, Gatekeeper) are not detected; use fixOwnership: false.

🤖 Generated with Claude Code

https://claude.ai/code/session_011HrjPjDpPgTdCFicDWQpvH

@jtatum jtatum left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing the restricted-namespace upgrade concern from #608. The namespace-aware default is useful; the remaining reliability concern is the Namespace-read failure path below. Validated locally with OpenClaw 2026.9.4 on Kubernetes 1.36.4.

Comment thread internal/controller/openclawinstance_controller.go Outdated
stubbi and others added 13 commits October 11, 2026 12:51
With spec.storage.fixOwnership unset, the controller reads the instance's
Namespace to decide whether init-data-owner is admissible. When that read
failed (for example RBAC without the namespaces "get" rule), it kept the
init container enabled, which could flip a working StatefulSet in a
restricted namespace to a Pod template admission rejects.

The read failure now aborts reconciliation before any managed resource is
touched, surfaces through the ReconcileFailed event and Ready condition,
and is retried. An explicit fixOwnership still bypasses the lookup.

Adds envtest coverage: a restricted-compatible instance whose namespace
read then fails keeps its StatefulSet unchanged, and explicit values skip
the lookup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In `watchNamespaces` mode, #618 places `get namespaces` in tenant Roles.
Namespace objects are cluster-scoped, so those grants cannot authorize
the Pod Security label lookup. The controller then retains the ownership
helper default, potentially producing a pod rejected by restricted
admission.

Move Namespace reads into the chart's existing cluster-scoped rule set
and ClusterRoleBinding. In namespaced mode, constrain `resourceNames` to
the configured watched namespaces. Cluster-wide mode permits reading any
Namespace. Neither mode gains Namespace list/watch/write permissions.

This PR targets `fix/data-owner-pss-aware` as a focused follow-up to
#618. It can be retargeted/rebased onto main after that PR merges. The
separate namespace-lookup failure handling review on #618 remains
relevant; this change fixes the chart permission grant.

Validation:
- A regression test renders the chart, applies its actual RBAC to a
disposable envtest API server configured with RBAC, and checks
authorization as the rendered operator ServiceAccount identity.
- The original #618 template fails the watched-Namespace read assertion.
The fixed template permits the two watched namespaces, denies
unwatched/operator namespaces, and denies Namespace list/watch/patch.
Cluster-wide mode is checked too.
- Full `make test`, `make lint`, and both Helm RBAC guard scripts pass.
CI installs Helm for the authorization test.

---------

Co-authored-by: Jannes Stubbemann <jannes.stubbemann@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	api/v1alpha1/openclawinstance_types.go
#	bundle/manifests/openclaw.rocks_openclawinstances.yaml
#	charts/openclaw-operator/templates/crds/openclaw.rocks_openclawinstances.yaml
#	config/crd/bases/openclaw.rocks_openclawinstances.yaml
#	docs/api-reference.md
#619 made every operator init container that writes pod storage use the
gateway's effective UID. Do the same for init-tmp-dir so the sticky /tmp
directory belongs to the user that writes to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Release Please has failed on every push since the repository moved from
paperclipinc to stubbi: vars.RELEASE_BOT_APP_ID and
secrets.RELEASE_BOT_PRIVATE_KEY were settings of the old organization, so
actions/create-github-app-token aborts with "Input required and not
supplied: app-id" before release-please runs.

- release-please.yaml and release.yaml mint the App token only when the
  App is configured, then fall back to RELEASE_PLEASE_TOKEN and finally
  GITHUB_TOKEN. With GITHUB_TOKEN, tag pushes and release publishing do
  not trigger other workflows, so release.yaml and operatorhub.yaml are
  dispatched explicitly (release.yaml gains a workflow_dispatch trigger
  and only runs on tag refs).
- The GHCR namespace for the image and the chart comes from one place,
  vars.IMAGE_NAMESPACE (default paperclipinc), used by release.yaml and
  .goreleaser.yaml. release.yaml previously used the repository owner
  (now stubbi) for signing and the chart while GoReleaser pushed to
  paperclipinc. A preflight fails the release if the namespace does not
  match the chart's default image.repository (#536).
- GHCR login accepts GHCR_PUSH_TOKEN / GHCR_PUSH_USER, needed to push
  into a namespace the repository owner does not own.
- GoReleaser creates the release in stubbi/openclaw-operator.
- make verify-signing accepts both the old and the new workflow identity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@stubbi

stubbi commented Oct 11, 2026

Copy link
Copy Markdown
Owner Author

Merge order note (maintenance pass, 2026-10-11). The open stubbi PRs are stacked so they apply cleanly in this order: #623 → #627 → #620 → #618 → #617 → #628 → #629. This is position 4 of 7. This branch has #620 (and everything below it) merged in, so until those land the diff here also shows their changes; once they are squash-merged, "Update branch" reduces it to this PR's own changes without conflicts. #623 is at the bottom because the required E2E check cannot pass without its sticky /tmp fix (OpenClaw >= 2026.9.6 crash-loops otherwise). Branch protection requires an up-to-date branch, so after each merge click "Update branch" on the next PR and let CI re-run before merging it.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants