Commit Graph
121 Commits
Author SHA1 Message Date
infinito-nexus-updater[bot] d1f5847f04 update(images): update Docker image versions (web-app-openbao) 2026-10-27 12:56:05 +00:00
infinito-nexus-updater[bot] 3808a2b7a8 update(images): update Docker image versions (web-app-openbao) 2026-10-26 20:54:11 +00:00
evangelostsak 8ec3d1792f feat(web-app-openbao): let a deployment put its hosts into meshes 2026-10-16 03:40:19 +02:00
kevinveenbirkenbachandClaude Opus 5 50b9f60c43 fix(prometheus): resolve the native-metrics scrape target per deployment mode
`native_metrics_target` was a Jinja filter, so it could not read
DEPLOYMENT_MODE: filters see no Ansible variables. It returned
`<services.<key>.name>:<port>` unconditionally, which is `matrix-synapse:9000`
for matrix. Compose serves that name; swarm does not, because `docker stack
deploy` ignores `container_name` and the alias never exists. This repository
says as much itself, through `lookup('compose_only', 'container_name', ...)`.
The target was therefore scraped and refused, and `playwright.spec.js:274`
reported `web-app-matrix: 1 matching target(s) reporting up != 1`, meaning one
matching target whose `up` value is not 1.

It becomes a lookup that delegates host resolution to `scrape_target`, which
was already mode-aware, so exactly one plugin knows the difference between the
two modes. The port has to be handed over explicitly: matrix exposes metrics on
9000 while `services.synapse.ports.internal.http` is 8008, so the existing
internal-port lookup cannot serve this case on its own.

`scrape_target` now refuses `port` together with an explicit `port_kind`.
Before, `port` won silently and the error text still named the
`ports.internal.<port_kind>` path that the call had never consulted.

`native_metrics_apps` drops an app that pins itself to compose through
`compose_mode_force` while the cluster runs swarm, because the scrape target
would then name the wrong object. Only the literal case: matrix declares that
override as a Jinja expression over its flavour, which this lookup cannot
resolve from the scraping role's variable scope, so variant 0 on swarm stays a
latent mismatch.

The two comment lines claiming `container_name:port` went with the change;
after it they state the opposite of what the code does.

Verified by mutation: with the swarm branch of `scrape_target` disabled, the
new test emits `matrix-synapse:9000`, the exact string CI reported.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-08 08:57:13 +02:00
kevinveenbirkenbachandClaude Opus 5.5 c925150848 fix(web-app-openbao): ship the LDAP auth method as an external plugin
OpenBao 2.7 removed the builtin LDAP auth engine from the server binary
(openbao/openbao GH-3882). `bao auth enable ldap` answered
"plugin not found in the catalog: ldap" and the deploy stopped there.

The role now builds its own image. A multi-stage Dockerfile copies the
openbao-plugin-auth-ldap binary out of the image declared as the
`auth-ldap` service entry into /opt/openbao/plugins. A second server
config file, plugins.hcl, names that directory and registers the plugin
declaratively. It sets plugin_auto_register = true explicitly: the image
starts the server with a config directory, and on that path 2.7.1 does
not apply the documented default, so without the line the catalog stays
empty. The Dockerfile sets the file mode in a helper stage instead of
COPY --chmod, which only BuildKit accepts.

The mount is enabled with -plugin-version=latest, so it follows whichever
plugin version the image ships. A mount created by the former builtin
engine is tuned and reloaded onto the plugin, which needs `update` on
sys/plugins/reload/backend in the ansible-admin policy.

The LDAP tasks move into tasks/addons/auth-ldap.yml and the plugin is
declared under meta/addons as a required addon bridged to `ldap`. Its
Playwright spec logs in through the mount and asserts that sys/auth
reports an external running plugin version.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 13:25:17 +02:00
kevinveenbirkenbachandClaude Opus 5.5 0eead47fd5 Merge remote-tracking branch 'origin/main' into update/docker-image-versions-web-app-openbao-20261002-0539f15
The validation job asserts that main is fully merged into the PR head,
so the branch has to carry the current main before CI can pass.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 06:23:36 +02:00
kevinveenbirkenbachandClaude Opus 5.5 d3ca143343 docs(roles): regenerate READMEs for Stalwart as the mail provider
MAIL_PROVIDER moved to web-app-stalwart without a make docs run, so every generated README still drew Mailu as the email dependency. This is the generator output for the current tree: the root role table and each role cosmos now name Stalwart, Mailu loses its dependents, and Stalwart gains its swarm marker, the Tor dependencies and the 0..0 edges.

The hand-written cosmos of svc-net-tor-smtp gives way to the generated one, which no longer shows the SOCKS5 hop through svc-net-tor because the role does not declare that dependency.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-03 19:57:30 +02:00
infinito-nexus-updater[bot] 44f57c82da update(images): update Docker image versions (web-app-openbao) 2026-10-02 20:09:27 +00:00
kevinveenbirkenbach 61af700d96 autoformat 2026-09-23 15:03:07 +02:00
evangelostsak 08855ff9f0 docs(openbao): cut the developer notes down to what gets read
Eighteen bullets to ten. Merges the four seal-and-recovery notes into two,
folds the three path traps into one, compresses the rotation block from six
sub-bullets to a paragraph, and drops the note on role-declared account
passwords entirely: the commit it was waiting on has landed, so the
workaround it described is no longer needed.

Nothing operational is lost. The seal key still says a raft backup is
undecryptable without the inventory, and the recovery key still says where it
lives and what it cannot do.
2026-09-21 17:45:45 +02:00
evangelostsak 6a01b190b0 chore(openbao): promote the role to beta
Twelve of twelve deploy jobs green across compose and swarm, clearnet and
tor, on every distro in the rotation.
2026-09-21 17:45:34 +02:00
evangelostsak 71e4437634 refactor(openbao): drop the activation guard from the alert fragment
The lookup now returns only scraped roles, so the fragment is rules and
nothing else.
2026-09-21 17:45:13 +02:00
evangelostsak e06a62e19b style(openbao): drop the narrative exception comments
Review had these removed across the role once already, and the 11 September
pass cleared every file that was flagged. The seal-key pin and the
recovery-key work then reintroduced the same pattern in nine new places.

Also removes two that predate the review and were never flagged: the
healthcheck note in meta/services.yml and the entrypoint note in
compose.yml.j2. Both are the same narrative-exception shape.

Param docs, file headers and the plain contract notes in the Playwright specs
stay: separate categories, none of them objected to. What the deleted
comments recorded lives in the role README.
2026-09-21 00:29:15 +02:00
evangelostsak 67ac5a3c59 docs(openbao): document the kept recovery key
Replaces the note that explained why the keys were discarded. Records where
the key lives and why there rather than the inventory, what it does and does
not protect, and the two costs: in swarm DIR_SECRETS is the NFS export, and
the key is root-equivalent.

Also corrects the MODE_RESET warning a second time. A reset is no longer a
lockout, because the key survives it and rebuilds the AppRole.
2026-09-20 01:23:13 +02:00
evangelostsak f1c217b685 feat(openbao): keep the recovery key and fall back to it
bao operator init issued a recovery key on the first deploy and the play read
only root_token from the JSON, dropping it. That left the AppRole as the sole
administrative path, so losing both halves of it ended the deploy for good on
an instance whose data was still readable.

The key is now persisted at init and loaded before the logins that may need
it. When neither the inventory AppRole nor the recorded one is accepted, the
run rebuilds the AppRole from it instead of failing. The hard fail remains for
the case where there is no key either.

The three first-deploy tasks move into a block: a third sibling sharing the
same when is what test_no_repeated_when_runs rejects.
2026-09-20 01:22:46 +02:00
evangelostsak b129189f50 feat(openbao): resolve the recovery key from the platform token store
DIR_SECRETS is the durable home a role actually has. The inventory would be
the natural one, but no role writes host_vars -- the CLI writes it before the
play. DIR_SECRETS outlives MODE_RESET and a replaced swarm manager, both of
which take .applied.json with them, and svc-bkp-secrets-2-local backs it up.
2026-09-20 01:22:34 +02:00
evangelostsak 7affa3d163 feat(openbao): add a break-glass path through the recovery key
Mints a root token with bao operator generate-root and hands over to
02_auth.yml, which re-pins both AppRole halves to the inventory values.

The four protocol steps run as separate tasks because the token comes back
XOR-wrapped with a one-time password and the image carries neither python nor
jq, so the CLI performs each step and Ansible parses the JSON -- the same
split the rest of the role already uses for bao status.
2026-09-20 01:08:56 +02:00
evangelostsak 9248d31e02 docs(openbao): document the pinned seal key
Records why the key is held out of the rotation, how to rotate it
deliberately, and what pinning costs. Also corrects the MODE_RESET warning:
with the seal key pinned the store stays readable, and what a reset destroys
is the AppRole record, leaving no administrative path back in.
2026-09-19 21:00:33 +02:00
evangelostsak 48333bb50a docs(openbao): restate why the key id is derived
The rationale rested on the key rotating every deploy, which it no longer
does. Deriving the id from the key is what keeps it in step with the key,
however rarely that happens.
2026-09-19 20:59:50 +02:00
evangelostsak fcfff81f64 docs(openbao): correct what the rotation record covers
The header claimed every generated credential rotates per deploy. The seal
key no longer does; it is recorded for the deploy that follows an operator
changing it.
2026-09-19 20:58:25 +02:00
evangelostsak 2b51f1cb62 fix(openbao): pin the seal key out of the per-deploy rotation
The static seal carries one generation. Rotating the key on every deploy
spends that slack continuously, so a run that skips a generation, or dies
between the two AppRole pins, leaves the raft store readable only from a
volume backup plus the key that wrapped it -- and the recovery keys that
would authorise a rekey are discarded at init.

Frequent rotation buys little here in return. The key lives in the
vault-encrypted inventory either way, and whoever holds that inventory holds
the AppRole too, so they reach the data without needing the seal key. An
operator now rotates it deliberately by changing the inventory value; the
previous_key path stays in place to carry that.

The AppRole keeps rotating per deploy. It is the live authentication path,
and the fallback login exists to absorb exactly that.
2026-09-19 20:58:05 +02:00
evangelostsak b20ad3c375 docs(web-app-openbao): the declared-user password trap is closed
The note told readers to set an explicit password until a commit landed that
pins one at inventory creation. That commit is an ancestor of this branch, so
the workaround it prescribed is no longer needed, and the branch URL it linked
answers 404 now that the branch is merged and deleted.
2026-09-19 20:07:23 +02:00
evangelostsak 3eaf133114 docs(openbao): document the per-deploy credential rotation
Replaces the note that treated a rotated seal key as local stale state to purge
around. It is the platform's behaviour, the role now carries it, and what is
left to know is where the window ends: two generations, with MODE_RESET the
concrete way to fall out of it, since it removes DIR_COMPOSITIONS while the
named volumes survive.
2026-09-15 02:15:56 +02:00
evangelostsak b7a1223481 feat(openbao): re-pin the AppRole and record what each deploy applied
Closes the rotation: the instance is moved onto this run's credentials and the
result is written to .applied.json, which is what lets the next deploy log in
before it re-pins in turn. The file sits in the compose project directory, which
already holds the seal key in plaintext in .env/env, so this adds no new
exposure class; it is written 0600 root-owned and last, after both pins, so a
run that dies partway leaves the previous generation intact.

custom-secret-id is not idempotent -- writing a SecretID the role already holds
answers 500 'SecretID is already registered' rather than doing nothing -- so the
swap is gated on the value having changed. The previously registered SecretID is
destroyed afterwards, without which the rotation would add a credential and
revoke none.
2026-09-15 02:15:23 +02:00
evangelostsak e6e27b2dac feat(openbao): let the AppRole login survive its own credentials rotating
A deploy is handed credentials the instance has not been told about yet, so the
inventory login is expected to fail on every rotation. Try it, fall back to what
the previous deploy recorded, and fail with a message naming the state file when
neither is accepted -- a bare 403 here reads like an unrelated defect.

Both logins are non-fatal so the guard is what reports the failure. Whichever
one succeeded also identifies the SecretID the instance actually holds, which is
the value the rotation has to destroy; taking it from the state file instead
would destroy the wrong one whenever the inventory credentials still worked.
2026-09-15 02:15:14 +02:00
evangelostsak 1a2bbb3c97 feat(openbao): export the previous seal key to the container while rotating
The seal stanza resolves both keys through env://, so the previous one has to
reach the container's environment for the rotation to resolve at all.
2026-09-15 02:14:59 +02:00
evangelostsak 4046dd2a0a feat(openbao): offer the previous seal key while the key is rotating
The static seal supports n-1 to n rotation. Emitting the previous pair only
while the two keys differ lets the node unseal with the old key and rewrap to
the new one; a first deploy still renders a single-key stanza.
2026-09-15 02:14:48 +02:00
evangelostsak 5c8721ee90 feat(openbao): read the applied credentials before the container is rendered
sys-stk-backend renders env.j2, which is what carries the previous seal key
into the container. Reading the applied state after that include leaves the
node with no way to decrypt its own store, so the read has to precede it.
2026-09-15 02:14:32 +02:00
evangelostsak 712de11283 feat(openbao): derive the seal key id and resolve the rotating credentials
The platform regenerates generated-algorithm credentials on every deploy, so
the seal key and AppRole this role is handed are not the ones the running
instance holds. Derive current_key_id from the key itself -- upstream requires
the identifier to match its key, and a derived id cannot drift out of step with
one. Carry the previously applied credentials and the two rotation predicates
alongside it, so the config render and the AppRole swap can both tell a
rotation from a steady state.
2026-09-15 02:14:25 +02:00
evangelostsak 912b6cda39 Merge infinito-nexus/core#454 (feature/ai-agent-employees) into feature/web-app-openbao
Only two files were touched by both sides. env.js merged automatically: the
incoming side owns recordVideoOptions and this branch had cherry-picked that
same function from it, so the result carries one definition. README.md is the
generated roles index and both sides append rows; resolved as a union, taking
the incoming side for the three rows it edits (OnlyOffice, Open WebUI, OpenClaw)
and keeping ours only for OpenBao, then re-sorted.

The incoming side adds lint rules and resources that this role then violated.
Five fixes, each the remedy the failing rule names:

* services.openbao.ports.local.http 8081 -> 8093; svc-ai-litellm now holds 8081.
* meta/networks.yml subnet 192.168.221.0/24 -> 192.168.222.0/24; svc-ai-mcp-adapter
  now holds 221, and the suggester only sub-allocates inside occupied /24s, so
  the next free block was taken directly.
* services.email bond 1 -> 0; the entry is disabled and the resource model skips
  it before reading the weight.
* README Quick Setup regenerated from the template it had drifted from.
* Credits link the implementer, matching how they are credited elsewhere.
* docs/requirements/035-mcp-proxy-expansion.md disposes of web-app-openbao as
  privileged_control_plane, which is what a secrets manager is, and the list
  count follows.

Verified against a worktree of #454 alone so the baseline is measured rather
than assumed: lint 501 passed with only the branch-name rule failing, which
fails identically on that baseline because the worktree branch is named
merge/*; unit 5665 passed with the same three host-environment failures the
baseline has; integration 324 passed, 0 failed.
2026-09-14 16:59:39 +02:00
evangelostsak e8693e69b1 docs(web-app-openbao): record the LAM-driven tier rotation and the stale-volume trap 2026-09-12 10:23:17 +02:00
evangelostsak 7a4f1104a6 test(web-app-openbao): aggregate the RBAC tier rotation spec 2026-09-12 10:23:00 +02:00
evangelostsak 436525ca24 test(web-app-openbao): prove every RBAC tier by moving biber through the groups in LAM
biber starts in no group and reaches no privileged surface. For each tier the
administrator signs into LAM, adds him to the role group through Multi edit,
and the spec asserts both the policy set he receives and the capability that
tier grants, then removes him and asserts the revoke. One account covers all
three tiers and the grant and revoke are observed rather than assumed.

LAM's group list renders posixGroup entries only, while the RBAC role groups
are groupOfNames under ou=roles, so Multi edit is the surface that reaches
them. It applies the change asynchronously, so the spec polls the resulting
policy set rather than LAM's own progress wording.
2026-09-12 10:22:51 +02:00
evangelostsak 15ad629375 test(web-app-openbao): expose LAM and the RBAC DN templates to Playwright 2026-09-12 10:22:41 +02:00
evangelostsak 1a54f10e0e test(web-app-openbao): deploy LAM in variant 0 so the RBAC rotation has an admin UI 2026-09-12 10:22:35 +02:00
evangelostsak 07d5a7180b docs(web-app-openbao): record the role-declared account password trap 2026-09-12 00:18:00 +02:00
evangelostsak df4be52aa6 test(web-app-openbao): drop the env keys the retired tier spec consumed 2026-09-12 00:17:42 +02:00
evangelostsak 840f59e080 test(web-app-openbao): stop aggregating the retired tier spec 2026-09-12 00:17:33 +02:00
evangelostsak 49201309d5 test(web-app-openbao): retire the account-based RBAC tier spec 2026-09-12 00:17:20 +02:00
evangelostsak 0053b07f2c fix(web-app-openbao): drop the dedicated RBAC tier accounts
baooperator and baoreader existed only to hold one RBAC group each, and a
role-declared account has no stable password until the inventory pins one, so
they needed a workaround to be logged in as at all. Moving biber through the
groups covers the same three tiers with an account whose password was never
broken, and additionally proves the grant and the revoke rather than a steady
state.
2026-09-12 00:17:07 +02:00
evangelostsak 3ebc686857 docs(web-app-openbao): correct how the RBAC tier accounts get their password 2026-09-11 23:29:45 +02:00
evangelostsak ef4b83014d fix(web-app-openbao): give the RBAC tier accounts ansible_become_password
Matches user-administrator and web-app-semaphore, the only other roles that
set a password in meta/users.yml, and the key the meta lint allow-lists. It
adds no exposure: user-administrator already assigns that password to the
administrator account, which holds the OpenBao administrator policy through
the same LDAP groups, so both accounts are strictly less privileged than one
that already shares the secret.
2026-09-11 23:28:58 +02:00
evangelostsak 85bd62de30 Revert "feat(web-app-openbao): generate the RBAC tier account passwords as role credentials"
meta/*.yml is static schema, not a runtime template, and a lookup into the
applications tree from there was the most runtime-dependent expression in any
meta file. The accounts take ansible_become_password instead.
2026-09-11 23:28:26 +02:00
evangelostsak 8eeb202932 docs(web-app-openbao): record the credential-backed RBAC account passwords 2026-09-11 21:16:06 +02:00
evangelostsak 6e96d45e66 fix(web-app-openbao): keep the RBAC tier accounts role-local with a stable password
The password resolves to the role's generated credential when OpenBao is
deployed and falls back to the previous random default otherwise, so no other
deployment changes.
2026-09-11 21:16:00 +02:00
evangelostsak dbb8503eeb feat(web-app-openbao): generate the RBAC tier account passwords as role credentials
A user with no explicit password inherits the strong_password fallback, which
is re-evaluated on every render with nothing persisted, so ldap_passwd and
playwright.env can receive different strings and the bind returns 403. Same
mechanism web-app-zammad uses for api_bot_password.
2026-09-11 21:15:53 +02:00
evangelostsak 7e0120bf2e docs(web-app-openbao): record why the RBAC test accounts live in the inventory 2026-09-11 20:55:00 +02:00
evangelostsak 484ebb31ac fix(web-app-openbao): move the RBAC test accounts to the inventory so they can log in
Declaring baooperator and baoreader in the role's meta/users.yml provisioned
them correctly into LDAP and Keycloak, but every login returned 403: a user
with no explicit password inherits "{{ 42 | strong_password }}", which is
re-evaluated through secrets.choice on each render with nothing persisted, so
the password written by ldap_passwd and the one rendered into playwright.env
were different strings.
2026-09-11 20:54:38 +02:00
evangelostsak c4e6cfb490 docs(web-app-openbao): record the RBAC tier coverage and the role-local alert rules 2026-09-11 18:38:07 +02:00
evangelostsak a9afc5b45c style(web-app-openbao): drop the narrative comments from the server config template 2026-09-11 18:37:19 +02:00