> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tinfoil.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Private secrets

> Deliver secrets to your Containers that Tinfoil never sees, released by a keyserver you run.

## Overview

[Standard secrets](/containers/secrets-and-env-vars) are stored by Tinfoil and
injected at deploy time, which means Tinfoil infrastructure can access their
values. Private secrets keep the value out of Tinfoil's reach: it lives in
your own secret store, behind a **keyserver** you run, and is released
directly to your secure enclave only after the enclave proves — with hardware
attestation — that it is running the exact release you approved.

Tinfoil never handles the value. It transits an attested TLS connection from
your keyserver into the enclave, and exists in plaintext only inside enclave
memory.

|                   | Standard secrets                  | Private secrets                                    |
| :---------------- | :-------------------------------- | :------------------------------------------------- |
| **Stored in**     | Tinfoil-managed encrypted storage | Your HashiCorp Vault, AWS Secrets Manager, or file |
| **Visible to**    | Tinfoil infrastructure            | Only your infrastructure and the enclave           |
| **Released when** | Deploy time                       | Every enclave boot, after attestation              |
| **Requires**      | Nothing extra                     | A keyserver you host                               |

## How it works

Your `tinfoil-config.yml` declares a `keyserver-url` pointing at your
keyserver. That file is part of what the enclave's attestation measures, which
has a simple consequence: the release you approve in your keyserver policy also
fixes which keyserver the enclave will talk to. Tinfoil cannot point a running
deployment at a different keyserver, and anyone who ships a config with a
different URL has made a different release, which your keyserver will refuse
until you pin its tag. Your enclave gets secrets only from your keyserver, and
your keyserver gives them only to code you approved.

On every boot:

1. The enclave requests a single-use nonce from your keyserver.
2. It builds a fresh hardware attestation document over that nonce. The
   document embeds everything needed to verify it: the CPU and GPU evidence,
   and the Sigstore artifacts that authenticate the running code as a
   specific release of your repo.
3. It sends the document to your keyserver over mutual TLS.
4. The keyserver verifies the document offline using the
   [Tinfoil SDK](https://github.com/tinfoilsh/tinfoil-go) — the same
   verification Tinfoil clients perform — and checks it against your policy:
   the pinned repo and release tag, and your deployment's domain. It answers
   only over the TLS connection that proves possession of the key inside the
   attestation, so a copied or relayed attestation document is useless.
5. Only if every check passes, the keyserver returns the secrets, and the
   enclave injects them into your container as environment variables.

```mermaid theme={"dark"}
sequenceDiagram
    participant E as Enclave<br/>(your release, on Tinfoil)
    participant K as Your keyserver
    participant S as Your secret store

    E->>K: POST /challenge
    K-->>E: Single-use nonce
    Note over E: Attest over the nonce:<br/>hardware quote, release identity, TLS key
    E->>K: POST /fetch: attestation + secret names<br/>(mutual TLS, keyed by the attested key)
    Note over K: Verify offline: nonce unused, hardware genuine,<br/>repo, tag and domain match the policy,<br/>TLS key matches this connection
    K->>S: Read the approved secrets
    S-->>K: Values
    K-->>E: Secrets, on the same connection
    Note over E: Injected as environment variables.<br/>Any failed check: nothing released, workload does not start.
```

The release decision is yours alone: your keyserver, your policy, your secret
store. Authorizing a new release is a one-line policy change (bump the pinned
tag). Enclaves that fail verification — wrong repo, wrong tag, debug mode
enabled, stale or replayed evidence — receive nothing.

## Requirements

* `cvm-version` 0.13.0 or later in `tinfoil-config.yml`.
* Your release workflow must use `tinfoilsh/measure-image-action` v0.10 or
  later.
* Keyserver v0.0.2 or later (`ghcr.io/tinfoilsh/keyserver:v0.0.2`).
* Every release needs a freshness witness before an enclave can boot from
  it. Tinfoil publishes one automatically for the latest release of each
  repository it deploys. If a brand-new release fails to start, wait a minute
  and relaunch.
* Your config repo must be public, like all Tinfoil Containers config repos
  (see [Private images](/containers/private-images) — the Docker image and
  your secrets stay private).
* The keyserver must be reachable from the enclave at every boot, serve
  `/challenge` and `/fetch` directly at the `keyserver-url` (redirects are
  refused), and present a TLS certificate from a publicly trusted CA
  (Let's Encrypt works; self-signed certificates are rejected).

## Run a keyserver

The reference keyserver is a small, auditable Go server:
[tinfoilsh/keyserver](https://github.com/tinfoilsh/keyserver). It fronts
HashiCorp Vault (KV v2), AWS Secrets Manager, or a local file for isolated
deployments.

Its policy pins which build may receive which secrets:

```yaml policy.yaml theme={"dark"}
workloads:
  my-app:
    repo: myorg/my-app-config
    tag: v1.0.0
    domain: my-app.myorg.containers.tinfoil.dev
    secrets:
      DATABASE_URL: { path: workloads/my-app/db, field: url }
```

`domain` is required; the keyserver refuses to load a policy without it.
Config repos are public, so anyone can deploy your exact repo and tag and
produce an identical, validly-attested measurement — without a domain pin,
that copy would also pass verification and receive your secrets. The domain
pin requires the caller to hold a CA-issued certificate for your deployment's
domain, which only your deployment can obtain, so a copy is refused.

See the [keyserver README](https://github.com/tinfoilsh/keyserver) for
running the server and connecting each backend.

## Wire your deployment

Declare the secrets on your container and point `keyserver-url` at your
keyserver:

```yaml tinfoil-config.yml theme={"dark"}
cvm-version: "0.13.0"

keyserver-url: https://keys.example.com

containers:
  - name: app
    image: "ghcr.io/myorg/app@sha256:<digest>"
    secrets:
      - DATABASE_URL
```

Setting `keyserver-url` switches the whole deployment to private secrets:
every secret declared in the config is fetched from your keyserver at boot,
and none can come from Tinfoil. Selecting a standard secret at launch for a
container that has `keyserver-url` set makes the boot fail closed rather than
silently substitute the value. Delivery is fail-closed in every other way too:
if any declared secret can't be fetched and verified, the workload does not
start.

<Note>
  Debug enclaves never talk to your keyserver. In debug mode the enclave
  ignores `keyserver-url` and takes every declared secret from the dashboard,
  so select them (and an SSH key) when you launch. The keyserver would refuse a
  debug enclave anyway, since its attestation says it is debuggable.
</Note>

Credentials for pulling private images are the one exception: Tinfoil still
supplies those, even with `keyserver-url` set.

## Operating notes

* **Fetches happen at every enclave boot** — first deploy, updates, and
  relaunches all create a new enclave that must reach your keyserver. A
  keyserver outage doesn't affect running enclaves, but blocks new ones from
  starting.
* **Rotation**: update the value in your secret store, then redeploy. The new
  enclave fetches the current value.
* **New releases**: update the pinned `tag` in your keyserver policy before
  deploying the new version, or the new enclave will be refused. The keyserver
  reads its policy at startup, so restart it after editing.
* **Audit**: the keyserver logs every release with the verified repo and tag,
  and every denial with its reason, giving you a complete record of who
  received which secrets.
* **Diagnosing a refused boot**: an enclave the keyserver refuses never
  becomes ready, and the deployment status does not yet show why. The
  keyserver's log has the reason.

For a minimal end-to-end example, see
[tinfoilsh/confidential-secrets-demo](https://github.com/tinfoilsh/confidential-secrets-demo).
