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