Skip to content

feat(openbao): central platform instance and per-tenant transit provisioning - #4177

Draft
myasnikovdaniil wants to merge 2 commits into
mainfrom
feat/openbao-central
Draft

myasnikovdaniil wants to merge 2 commits into
mainfrom
feat/openbao-central

Conversation

@myasnikovdaniil

Copy link
Copy Markdown
Contributor

What this PR does

Phases 1 and 2 of #2787, the platform side of tenant OpenBAO auto-unseal. Two commits: the central instance, and the reconciler that provisions per tenant against it. The tenant half is not here, see the bottom.

Worth knowing before reading the diff, because it is why a new package exists at all: packages/system/openbao has never been a running instance. Its component carries no install section, and package_reconciler.go creates a HelmRelease only for components that have one, so nothing ever stood it up. It is the workload chart a tenant OpenBAO renders from, published for that purpose and nothing else, which is why its values are a five line stub. The platform instance therefore gets its own package rather than being loaded into that one.

packages/system/openbao-central is HA raft over cert-manager issued TLS, with a Shamir seal because no cloud KMS is assumed on bare metal. A one time init Job persists the unseal keys and root token, a long running unsealer re-unseals pods after a restart, and a setup Job enables the transit engine and kubernetes auth that the per-tenant seals will use. It wraps the same published chart artifact a tenant instance uses instead of vendoring the upstream chart a second time, so one make update in packages/system/openbao moves both consumers and they cannot drift to different upstream versions.

Naming there is load-bearing and nothing in Go checks it: the reconciler reaches this instance at https://openbao-system.cozy-openbao.svc:8200 and reads openbao-system-keys and openbao-system-ca from cozy-openbao. central_test.yaml pins the naming, the https scheme, the apiserver egress label and the raft topology, so a values edit turns into a red test instead of broken tenant provisioning.

Replicas default to 3 and go down to 1, which a single node install needs. Raft stays the backend at every count, including 1: the backend a barrier was created under cannot be changed afterwards, so scaling 1 to 3 has to be an in-place addition of voters rather than a file to raft migration nobody can perform. At one replica the retry_join set is omitted, because a lone node would retry a self join it cannot complete until the init Job promotes it, and the upstream chart's REQUIRED hostname anti-affinity is dropped, which is the other half of why a single node schedules at all. Rendering at 3 replicas is byte identical to the hardcoded config it replaces.

The instance also carries policy.cozystack.io/allow-to-apiserver. It runs service_registration "kubernetes", and without that label cozystack's CiliumNetworkPolicy does not admit the egress, so the pods hang at startup with the listener open and nothing answering (#2793).

The reconciler watches inner OpenBAO HelmReleases labelled apps.cozystack.io/transit-unseal and ensures, per instance, a transit key, an unseal policy scoped to that key, and a kubernetes-auth role bound to the instance's server ServiceAccount. It then publishes the central CA as the <name>-transit-ca ConfigMap, a public certificate rather than a Secret. No token is minted or delivered: the instance exchanges its own projected ServiceAccount token for a transit token at every boot, so there is no long-lived credential in the tenant namespace to store, rotate or leak. Nothing emits that label yet, so the reconciler is inert until the tenant chart grows a transit seal.

One cost to be explicit about rather than discover later: this installs a platform OpenBAO on every paas install, and with the tenant default in the next PR being the transit seal it cannot be made opt-in without breaking that default. Three replicas is the default and one is supported, so a small install can pay less, but it does pay something.

Not in this PR, and deliberately: the tenant seal stanza, the auto-init Job on the tenant side, the migration that pins existing instances, and the e2e. Those are the next PR, which also needs #4168 for the seal.type enum it extends with a third value.

Verified: make unit-tests green on this branch, go build ./... and go test ./internal/operator/... clean, the central package renders at 1, 3 and 5 replicas and refuses 0, and the new suite was mutation checked (turning TLS off, keeping the anti-affinity at one replica and allowing a self join each turn a test red).

Screenshots

No UI change.

Downstream repositories

Walked the trigger map in docs/agents/contributing.md against the diff. One hit, left unticked because the follow-up is not filed yet and I would rather a human decide whether it belongs on next now or with the tenant half: this adds a platform component, which the map routes to the website's content/en/docs/next/guides/platform-stack/_index.md and operations/configuration/licenses.md.

Nothing else matches. The package is under packages/system/, not packages/apps/ or packages/extra/, so no reference page is generated for it and the website Makefile app lists are unaffected. No app values.schema.json, no ApplicationDefinition, no release.prefix, no version enum and no hand-typed CRD changed, so terraform-provider-cozystack has nothing to mirror. packages/core/installer/values.yaml, the platform variants and the node prerequisites are untouched, so ansible-cozystack is unaffected. The packages/{apps,system,extra,core} split, the hack/ layout and the cozy-* namespace names are unchanged, so ccp keeps its anchors.

Release note

feat(openbao): the platform now runs a central OpenBAO (`packages/system/openbao-central`) in `cozy-openbao`: HA raft over cert-manager TLS, Shamir seal with automatic init and re-unseal, and a transit engine that tenant instances will use for auto-unseal. Replicas default to 3 and are configurable down to 1 for single-node installs. The cozystack operator provisions a transit key, a scoped unseal policy and a kubernetes-auth role per tenant instance, and publishes the central CA as a ConfigMap, so no unseal token is ever stored in a tenant namespace.

packages/system/openbao has never been a running instance: its component
carries no install section, and package_reconciler.go creates a
HelmRelease only for components that have one. It is the workload chart a
tenant OpenBAO renders from, published for that purpose and nothing else,
which is why its values are a stub.

So the platform instance that #2787 asks for needs its own package, and
this is it. HA Raft over cert-manager TLS, a Shamir seal because no cloud
KMS is assumed on bare metal, a one-time init Job that persists the
unseal keys and root token, a long-running unsealer that re-unseals pods
after a restart, and a setup Job that enables the transit engine and
Kubernetes auth the per-tenant seals will use.

It wraps the same published chart artifact a tenant instance uses rather
than vendoring the upstream chart again, so one `make update` in
packages/system/openbao moves both consumers and they cannot drift to
different upstream versions.

Naming here is load-bearing rather than cosmetic, and nothing in Go
checks it: the transit reconciler reaches this instance at
https://openbao-system.cozy-openbao.svc:8200 and reads openbao-system-keys
and openbao-system-ca from cozy-openbao. central_test.yaml pins the
naming, the https scheme, the apiserver egress label and the raft
topology so a values edit cannot quietly break tenant provisioning.

Replicas default to three and are configurable down to one. Raft stays
the backend at every count, because the backend a barrier was created
under cannot be changed afterwards, so scaling 1 -> 3 has to be an
in-place addition of voters. At one replica the retry_join set is omitted
and the upstream chart's required hostname anti-affinity is dropped,
which is what lets a single-node install schedule at all.

Refs #2787

Assisted-By: LLM
Signed-off-by: Myasnikov Daniil <[email protected]>
…al OpenBAO

Phase 2 of #2787: the platform side of tenant auto-unseal. A tenant
OpenBAO cannot be handed a transit seal until something has created its
key on the central instance and given its pods a way to authenticate, and
that has to be reactive because instances come and go with tenants.

The reconciler watches inner OpenBAO HelmReleases carrying
apps.cozystack.io/transit-unseal and, per instance, ensures a transit key,
an unseal policy scoped to that key, and a Kubernetes-auth role bound to
the instance's server ServiceAccount. It then publishes the central CA as
the <name>-transit-ca ConfigMap, which is a public certificate rather than
a Secret.

No token is minted or delivered. The instance exchanges its own projected
ServiceAccount token for a transit token at every boot, so there is no
long-lived credential in the tenant namespace to store, rotate or leak.
The CA ConfigMap is owner-referenced to the HelmRelease, so it is
garbage-collected with the instance.

Nothing emits that label yet, so this is inert until the tenant chart
grows a transit seal. It ships here because it is the platform half of
the feature and belongs with the central instance rather than with the
chart that consumes it.

Refs #2787

Assisted-By: LLM
Signed-off-by: Myasnikov Daniil <[email protected]>
@coderabbitai

coderabbitai Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added size/XXL This PR changes 1000+ lines, ignoring generated files area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review kind/feature Categorizes issue or PR as related to a new feature labels Sep 9, 2026
Aleksei Sviridkin (lexfrei) pushed a commit that referenced this pull request Sep 30, 2026
Every openbao PR lands on area/uncategorized, because the labeler has no
scope entry for it and there would be no label to apply if it did. #4168
and #4177 are both sitting on the fallback right now.

labels.yml sets the bar for a new area at three open issues on the topic,
and openbao is at three: #2787, #2483 and #2793. It also follows the
shape already used for a single shipped component, next to area/kamaji,
area/keycloak and area/cilium.

Maps the plain `openbao` scope and `openbao-central` alongside it, since
the platform instance lives in its own package and gets its own scope.

Assisted-By: LLM
Signed-off-by: Myasnikov Daniil <[email protected]>
Aleksei Sviridkin (lexfrei) added a commit that referenced this pull request Sep 30, 2026
…4182)

## What this PR does

Every openbao PR lands on `area/uncategorized`. The labeler has no scope
entry for it, and there would be no label to apply if it did, so both
#4168 and #4177 are sitting on the fallback right now.

`labels.yml` puts the bar for a new area at three open issues on the
topic, and openbao is at three: #2787, #2483 and #2793. The shape
follows what is already there for a single shipped component, next to
`area/kamaji`, `area/keycloak` and `area/cilium`. The mapping takes the
plain `openbao` scope and `openbao-central` alongside it, because the
platform instance lives in its own package and carries its own scope.

Checked that every `area/*` and `kind/*` the labeler references is
declared in `labels.yml`: 27 referenced, none missing, before and after
this change. zizmor and the pre-commit hooks pass, and the embedded
script still parses under github-script semantics.

### Screenshots

No UI change.

### Downstream repositories

Walked the trigger map against the diff. Nothing matches: the change is
two entries under `.github/`, and no trigger in the map is keyed on the
label set or the labeler. No package added, renamed or removed, no
platform component or variant, no `values.schema.json`, no
`ApplicationDefinition`, no `hack/` layout change, no namespace rename.

- [x] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up:
- [ ]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up:
- [ ]
[cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack)
- follow-up:
- [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up:
- [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up:
- [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) -
follow-up:
- [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) -
follow-up:
- [ ]
[cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server)
- follow-up:
- [ ]
[cozystack/external-apps-example](https://github.com/cozystack/external-apps-example)
- follow-up:
- [ ] [cozystack/examples](https://github.com/cozystack/examples) -
follow-up:
- [ ] [cozystack/community](https://github.com/cozystack/community) -
follow-up:

### Release note

```release-note
NONE
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added an `area/openbao` label for tracking issues and pull requests
related to OpenBAO.
- Updated automatic pull request labeling to apply this label to changes
scoped to OpenBAO and OpenBAO Central.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review kind/feature Categorizes issue or PR as related to a new feature size/XXL This PR changes 1000+ lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant