multica_agent_host: op shim reads the service account token (WDWL-37)
Deliver vault_op_service_account_claude to the agent hosts and install a role-owned ~multica/.local/bin/op shim that exports OP_SERVICE_ACCOUNT_TOKEN to /usr/bin/op only. op run / op plugin run get env -u spliced after "--" so the child never sees the token. The daemon unit is untouched (WDWL-18). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0d674e0c4b
commit
78ac06378e
5 files changed
+145
-20
No files matched your search
@@ -38,7 +38,7 @@ otherwise clean at the `production` profile.
|
||||
| `multica` CLI, pinned, from the GitHub release with `checksums.txt` verification | yes |
|
||||
| Agent CLIs (`claude`, `hermes`, …) | yes |
|
||||
| 1Password CLI (`op`), from 1Password's signed apt/yum repository | yes (`multica_op_install`) |
|
||||
| 1Password service account token, as a 0600 file for the daemon user | opt-in (`multica_op_deliver_token`); not exported to the daemon |
|
||||
| 1Password service account token, as a 0600 file for the daemon user, plus an `op` shim that reads it | opt-in (`multica_op_deliver_token`); not exported to the daemon |
|
||||
| `workspaces_root`, on the intended mount, writable by the daemon user | yes |
|
||||
| Multica authentication (`multica login --token`) | yes |
|
||||
| CLI config (`server_url`, `app_url`, `workspaces_root`, `device_name`, …) | yes |
|
||||
@@ -241,6 +241,7 @@ See `defaults/main.yml` for the annotated list. The ones you will actually set:
|
||||
| `multica_op_install` | `true` | 1Password CLI from 1Password's apt/yum repo. |
|
||||
| `multica_op_version` | `""` | `""` tracks the repo (upgrades on every run); set e.g. `2.41.1` to pin. |
|
||||
| `multica_op_deliver_token` | `false` | Write `multica_op_service_account_token` to `multica_op_token_file`. |
|
||||
| `multica_op_shim` | `true` | With the token delivered, install the `op` shim at `multica_op_shim_path`. |
|
||||
|
||||
### Adding an agent CLI
|
||||
|
||||
@@ -399,13 +400,13 @@ The role can deliver the token, but by default does not:
|
||||
|
||||
```yaml
|
||||
multica_op_deliver_token: true
|
||||
multica_op_service_account_token: "{{ vault_op_service_account_tokens[inventory_hostname_short] }}"
|
||||
multica_op_service_account_token: "{{ vault_op_service_account_claude }}"
|
||||
```
|
||||
|
||||
writes it to `multica_op_token_file`
|
||||
(`~multica/.config/op-service-account/token`, directory 0700, file 0600,
|
||||
owned by the daemon user). That is all it does. **The role does not put the
|
||||
token into the daemon's environment**, for two reasons:
|
||||
owned by the daemon user) and installs an `op` shim. **The role does not put
|
||||
the token into the daemon's environment**, for two reasons:
|
||||
|
||||
1. The daemon's systemd user unit is written and owned by the multica CLI
|
||||
(`multica daemon autostart`). The role must not template or edit it, or it
|
||||
@@ -413,16 +414,35 @@ token into the daemon's environment**, for two reasons:
|
||||
2. Anything in the daemon's environment is inherited by every agent process
|
||||
it spawns, where a casual `env` or `printenv` puts it in a transcript.
|
||||
|
||||
How the token reaches `op` is a separate decision, not yet made. Until then
|
||||
an agent can use it explicitly:
|
||||
`op` reads the token only from `OP_SERVICE_ACCOUNT_TOKEN`; it has no
|
||||
token-file option. So the role installs a shim at `multica_op_shim_path`
|
||||
(`~multica/.local/bin/op`, root-owned 0755), which the CLI-written unit
|
||||
already puts first on `PATH`. The shim:
|
||||
|
||||
- reads the token file and exports `OP_SERVICE_ACCOUNT_TOKEN` to
|
||||
`multica_op_binary` (`/usr/bin/op`) only, then `exec`s it. Agents call plain
|
||||
`op run` / `op inject` / `op read` and the token never enters their shell;
|
||||
- fails closed (exit 78, no token in the message) when the file is missing
|
||||
or empty;
|
||||
- for `op run` and `op plugin run`, splices `/usr/bin/env -u
|
||||
OP_SERVICE_ACCOUNT_TOKEN` in after the first `--`. `op run` otherwise
|
||||
hands its whole environment, token included, to the command it starts.
|
||||
Because it needs that `--` to find the command, the shim refuses
|
||||
`op run <command>` without one (exit 64).
|
||||
|
||||
The play checks, as the daemon user, that `op` resolves to the shim and that
|
||||
the token is not in that environment.
|
||||
|
||||
The shim keeps the token out of transcripts by accident; it is not a
|
||||
boundary. Any process running as the daemon user can read the token file on
|
||||
purpose. What actually limits exposure is the service account's vault access
|
||||
in 1Password: keep it read-only. Check it as the daemon user:
|
||||
|
||||
```bash
|
||||
OP_SERVICE_ACCOUNT_TOKEN="$(cat ~/.config/op-service-account/token)" op run -- <cmd>
|
||||
op vault list --permission read_items # vaults it can read
|
||||
op vault list --permission edit_items # should be empty
|
||||
```
|
||||
|
||||
`op` reads the token only from `OP_SERVICE_ACCOUNT_TOKEN`; it has no
|
||||
token-file option.
|
||||
|
||||
## Known gaps
|
||||
|
||||
- **Provider CLI login.** Unavoidable for subscription auth.
|
||||
@@ -444,7 +464,7 @@ token-file option.
|
||||
| `multica_auth` | `multica login` |
|
||||
| `multica_daemon` | autostart + start |
|
||||
| `multica_verify` | the assertions |
|
||||
| `multica_op` | 1Password CLI, and the token file when enabled |
|
||||
| `multica_op` | 1Password CLI, and the token file and shim when enabled |
|
||||
| `multica` | all of the above |
|
||||
|
||||
Preflight and the service-account step are tagged `always`: they build
|
||||
|
||||
Reference in new issue
Block a user