---
id: w1/m16/t003
title: Secret files — Core + `/etc/secrets` file-mount projection; REST/GraphQL/MCP + Environment UI
worker: worker1
status: done
estimate: 40m
depends_on: [w1/m16/t001]
---

## Objective

Add per-service secret files: files (not KEY=VALUE) that are mounted into the running app at `/etc/secrets/<name>` — for certs, service-account JSON, and other file-shaped credentials. Core exposes list/get/put/delete of a service's secret files; values live in OpenBao (source of truth) and project as file entries into the same per-app `<name>-env` Secret, wired into the pod as a volumeMount at `/etc/secrets`. Expose over REST (`/v1/services/{id}/secret-files`), GraphQL, MCP, and the service Environment tab.

## Context

- Extends the same `secrets` feature and OpenBao path used for env vars (docs/ADR013-secrets.md; docs/ADR006-bex-api.md "Env vars"). Store secret-file contents in OpenBao alongside the env map (e.g. a sibling path per service), project them into the existing `<name>-env` Secret as additional data keys.
- Render's shape: `/v1/services/{id}/secret-files` — files named, mounted at `/etc/secrets/<name>` (inbox note w1/010).
- The env-vars path already materializes the `<name>-env` Secret and rolls pods via `spec.restartedAt`; secret files ride that same Secret + roll, and additionally need a **volumeMount** at `/etc/secrets` — this touches the operator's pod spec (a Secret-backed volume + mount), distinct from the `envFrom` wiring env vars use.
- Decide how the operator distinguishes file entries from env entries in the one Secret (e.g. a key-name convention or a separate Secret key set) so `envFrom` doesn't inject file contents as env vars.
- Fail-closed + authz: same as env vars — 503 when OpenBao unset, sensitive-read to read, manage to write.

## Steps

1. Core verbs on the secrets feature `Service`: `ListSecretFiles`/`GetSecretFile`/`PutSecretFile`/`DeleteSecretFile(serviceId, name)`, storing contents in OpenBao.
2. Projection: materialize file entries into the `<name>-env` Secret distinctly from env vars, and wire the operator's pod spec to mount them as a Secret volume at `/etc/secrets/<name>`; roll pods so mounts take effect.
3. Adapters: REST `/v1/services/{id}/secret-files` (matching Render's shape) + GraphQL + MCP tools, all delegating to the same Core verbs.
4. Dashboard: a secret-files section on the service Environment tab (add/edit/remove a named file).

## Files

- `lego/backend/internal/secrets/service.go` — secret-file Core verbs.
- `lego/backend/internal/secrets/rest.go`, `graphql.go`, `mcp.go` — secret-file adapter fragments.
- `lego/operator/` — pod-spec projection: Secret-backed volume + volumeMount at `/etc/secrets` (App reconcile path).
- `lego/types/` — any `App.spec` field needed to signal the file mount, if not derivable from the Secret.
- `dashboard/` — secret-files section on the Environment tab.

## Acceptance criteria

- [ ] `List`/`Get`/`Put`/`Delete` secret-file Core verbs round-trip a named file's contents via OpenBao.
- [ ] A put secret file is projected into the `<name>-env` Secret and mounted in the running pod at `/etc/secrets/<name>`; env vars in the same Secret are unaffected (not injected as env, file not injected as env).
- [ ] REST `/v1/services/{id}/secret-files` + GraphQL + MCP surfaces exist and delegate to the same Core verbs.
- [ ] Dashboard Environment tab can add/edit/remove a secret file.
- [ ] 503 when OpenBao unset; 403 for a tuple-less key.

## Out of scope

- Secret files attached to environment groups (env-group values in t001/t002 are var-only for this milestone).
- Render parity audit (t004) and dedicated tests (t006).
