`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>
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>
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>
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>
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.