Daedro / Handbook
Chapters

Private Secrets pilot

What works in code

The secrets command connects these steps without accepting secret paths or workspace IDs from the browser:

  1. Sign in using an administrator-provisioned OpenBao userpass account.
  2. Complete the configured OpenBao TOTP enforcement challenge.
  3. Resolve the authenticated entity ID to its configured workspace.
  4. Click Run approved MCP. The broker issues a fresh one-use SecretID (at most 60 seconds), logs in with its deployment AppRole and reads one pinned KV v2 version. The service token lifetime cannot exceed five minutes.
  5. Revoke the OpenBao token before starting the MCP. On any failure, deny execution. Pass only the needed key to the reviewed MCP process.
  6. Show a run reference, duration and success/failure. Raw MCP content and secret values are not returned to the browser.

The server uses HTTPS on loopback, secure HttpOnly SameSite cookies, session rotation after MFA, CSRF tokens and exact origin checks. Sign-in/verification is limited to ten attempts per minute across this small private installation. Sessions last at most five minutes; logout and shutdown attempt revocation.

Operator configuration

Build with make build. Store configuration and the scoped provisioner token outside Git in owner-only files (mode 0600). The TLS private key must also be owner-only. Use a trusted certificate for the browser; never disable TLS checks.

{
  "endpoint": "https://127.0.0.1:8820",
  "ca_file": "/private/daedro/bao-ca.pem",
  "certificate": "/private/daedro/hub-cert.pem",
  "key": "/private/daedro/hub-key.pem",
  "mfa_method_id": "REPLACE_WITH_ENFORCED_TOTP_METHOD_ID",
  "provisioner_token_file": "/private/daedro/issuer-token",
  "entities": {"REPLACE_WITH_OPENBAO_ENTITY_ID": "demo"},
  "deployment": "source-fixture",
  "bindings": [{
    "workspace": "demo",
    "deployment": "source-fixture",
    "ref": {"Workspace": "demo", "Name": "source-key", "Version": 1},
    "role_id": "REPLACE_WITH_ROLE_ID",
    "app_role": "demo-source-fixture"
  }],
  "mcp": {
    "executable": "/private/daedro/approved-source-mcp",
    "sha256": "REPLACE_WITH_VERIFIED_EXECUTABLE_SHA256",
    "args": [],
    "tool": "read_fixture",
    "arguments": {"key": "public-demo"},
    "secret_env": "SOURCE_API_KEY",
    "environment": {}
  }
}

These are placeholders, not a deployable preconfigured account. The configured MCP must implement stdio MCP, expose the selected read-only tool, and consume that environment variable. Only the operator can choose the executable, digest, arguments and environment. The executable must not be group/world writable or a symlink. Its digest is verified again on every execution.

./bin/daedro-mcp-hub secrets --config /private/daedro/secrets.json --port 8794

Open https://127.0.0.1:8794/. For a remote installation, use an authenticated SSH tunnel preserving this origin. Do not publish this pilot with a public reverse proxy. The catalog on port 8791 does not grant Secrets access.

OpenBao provisioning requirements

Enable KV v2 at daedro and AppRole at auth/approle. Store the value in the value field at daedro/data/demo/source-key. Provision the workload role with only read on that exact path, service tokens with TTL/max/explicit-max <=300s, and single-use SecretIDs with TTL <=60s. No wildcard tenant paths.

The issuer token must have only update on auth/approle/role/demo-source-fixture/secret-id; no root, secret-read or policy administration privileges. Rotate its private file atomically. Bind the human entity to enforced TOTP on the userpass authentication mount. The human token requires no workload-secret read permission. Set human service-token TTL to at most 300 seconds. A password-only login is rejected even if OpenBao accepts it.

Provisioning, TOTP enrollment and offline recovery handling require a separate operator ceremony. The hub does not persist root tokens or unseal shares.

Boundaries and unfinished deployment

  • This runner is for operator-reviewed MCPs, not untrusted uploads. It is a subprocess, not a container sandbox. It does not enforce a network allowlist.
  • Environment injection is a compatibility mode: host administrators and the MCP can access plaintext. Go cannot guarantee removal of every memory copy.
  • Revoking an OpenBao token does not revoke a provider API key already read.
  • Human-token revocation outside the hub is not rechecked on each request; the bounded local session can remain usable until expiry. Workload grants and AppRole access are checked afresh for every run.
  • Requests are serialized in this private UI; this is not a scalable public multi-tenant control plane. There is no self-service enrollment, key upload, recovery UI, WebAuthn or off-host application audit pipeline yet.
  • Automated checks use synthetic TLS fixtures and an actual fixture MCP process. They do not prove live cloud MFA or a real provider request.
  • The existing Google VM is initialized and unsealed, and owner enrollment has completed. Browser MFA and a keyed MCP run still need live acceptance. No new cloud resources are required by this command and none are provisioned automatically.

See test handoff for acceptance gates and cloud rollout for the recorded infrastructure state.

Owner enrollment helper

python3 deploy/openbao/enroll-pilot.py operates through the existing verified IAP tunnel at 127.0.0.1:8821. The owner enters the recovery passphrase locally, creates the owner password, adds the displayed enrollment seed to an authenticator, and supplies one TOTP code. Do not share the seed or terminal screenshot. The helper refuses existing pilot mounts or an existing enrollment directory; partial failures require operator inspection rather than overwriting credentials.

It provisions a synthetic keyed fixture, exact-path AppRole and 24-hour scoped issuer, writes private configuration under ignored .local/secrets-owner, then starts the local HTTPS UI on port 8794. The OpenBao server remains in Google Cloud; the broker and approved fixture run locally for this acceptance step. Browser certificate trust must be checked separately. Root recovery remains encrypted; root-token revocation is still pending independent live acceptance.

Owner enrollment completed on November 4, 2026. The private TLS tunnel and unsealed status were rechecked during browser-login repair. This does not yet establish a successful browser MFA session or a live keyed MCP run.

Sign-in troubleshooting

Use owner and the hub password created during enrollment, then the current six-digit authenticator code. The encrypted recovery-file passphrase belongs only in the local recovery tools, never in the sign-in form.

The form response uses Referrer-Policy: same-origin. Using no-referrer makes browser form POSTs carry Origin: null, which the strict origin check rejects. Do not fix this by accepting null origins or removing CSRF checks. A Chrome submission with deliberately invalid credentials now reaches the normal sign-in error page instead of a bare Request denied response. Expired forms, including forms opened before a server restart, return an explanation and a fresh sign-in link; submissions are never replayed.

Chrome ignores macOS trust entries constrained by a policy hostname. A successful security verify-cert alone therefore does not establish Chrome trust. Verify the browser indicator separately. A dedicated non-CA leaf certificate containing only the loopback IP can constrain trust without trusting a general-purpose CA. Installing it requires macOS authorization; do not bypass browser warnings.

References: form Origin behavior, Chromium macOS trust constraints.

Local TLS verification — November 4, 2026

The owner approved macOS trust for a dedicated self-signed leaf certificate with CA:FALSE, server-auth usage and the sole SAN 127.0.0.1. The private pilot now uses this certificate; Chrome visibly reports a secure connection. No general-purpose CA was trusted. The certificate expires December 4, 2026 at 06:55 UTC and must be renewed and trusted before then. Certificate and key files remain in ignored local state. Owner browser MFA acceptance remains pending.