# 05: User Secrets

## Outcome

Declare one author-owned User Secret by name, prove that a missing value stops
execution before a guest starts, configure the value through a write-only
path, and run without exposing it.

## Prerequisites

- Complete Tutorials 01 through 04.
- Permission to create lesson objects and to manage your own User Secrets.
- A protected local file that contains one disposable lesson value. The value
  must not be a real credential and must not be committed.
- `jq` on the local system if you use the optional LEMCSSH path.
- About 25 minutes. No external service is used.

## Files

- `Dockerfile` and `run.sh` own the recipe image.
- `cookbook.yaml` contains only the name `TUTORIAL_DISPOSABLE_TOKEN` under
  `environment.secrets`.
- `run.sh` checks that injection occurred and never prints the value, its
  length, a hash, or a transformed form.

## Web

1. Create `tutorial-user-secrets`. Add an image-scoped Builder Test State
   Bundle that writes `TUTORIAL_DISPOSABLE_TOKEN`, and compile the image with
   that exact bundle revision. Create the lesson Cookbook, bind its verified
   build, publish it, and create a test App. The bundle is Builder test state;
   it does not configure your User Secret.
2. Before you create the User Secret, run **Check Secret Preflight**. Confirm
   that preflight reports the missing name and that no recipe guest starts.
3. Open the username menu, select **Secrets**, and create
   `TUTORIAL_DISPOSABLE_TOKEN`. Paste a unique disposable value into the masked
   value field. Use `<redacted>` only as the metadata example.
4. Run the recipe again. Inspect output, logs, status, and artifact metadata.
   None can contain the submitted value.
5. Disable the disposable User Secret after the successful proof.

## LEMCSSH

The Builder must run `run.sh` before it can qualify the image. Create an
image-scoped Test State Bundle for that run. `jq --rawfile` reads the
disposable value without putting it in argv. Record `BUNDLE_UUID` and
`BUNDLE_REVISION` from the response. The bundle is separate from the User
Secret, so the first App run still proves the missing-secret preflight.

The User Secret create operation also reads the value only from bounded stdin.
Protect and delete `DISPOSABLE_VALUE_FILE` outside this repository.
The complete remote create command is one quoted argument. This lets stock
OpenSSH preserve the required `<redacted>` marker as metadata.

```sh
jq -n --rawfile value DISPOSABLE_VALUE_FILE \
  '{schema:"lemc.builder-test-state-bundle/v1",name:"tutorial-05-builder-state",secret_writes:[{name:"TUTORIAL_DISPOSABLE_TOKEN",expected_version:0,value_base64:($value|@base64)}]}' |
  ssh LEMC_ALIAS builder images bundles create IMAGE_UUID \
    --idempotency-key REQUEST_UUID --format=json

ssh LEMC_ALIAS builder images compile-run IMAGE_UUID \
  --expected-revision SOURCE_REVISION \
  --bundle BUNDLE_UUID --bundle-revision BUNDLE_REVISION \
  --idempotency-key REQUEST_UUID --follow --format=json

ssh LEMC_ALIAS jobs run app --uuid APP_UUID --page 1 \
  --scope individual --recipe check-secret-preflight \
  --idempotency-key REQUEST_UUID --format=json

ssh LEMC_ALIAS \
  'auth secrets create --name TUTORIAL_DISPOSABLE_TOKEN --description Disposable-Tutorial-05-value --example "<redacted>" --idempotency-key REQUEST_UUID --format=json' \
  < DISPOSABLE_VALUE_FILE

ssh LEMC_ALIAS auth secrets show TUTORIAL_DISPOSABLE_TOKEN \
  --format=json

ssh LEMC_ALIAS jobs run app --uuid APP_UUID --page 1 \
  --scope individual --recipe check-secret-preflight \
  --idempotency-key REQUEST_UUID --follow --render plain

ssh LEMC_ALIAS auth secrets disable TUTORIAL_DISPOSABLE_TOKEN \
  --expected-version SECRET_VERSION --confirm \
  --idempotency-key REQUEST_UUID --format=json
```

## Expected result

The first action fails preflight with the missing secret name and starts no
guest. `auth secrets show` returns metadata and configured state only. The
second action reaches terminal success and emits:

```text
secret preflight passed; value was not printed
```

The disabled record remains value-free in every read response.

## Cleanup and retry

Disable the User Secret, delete the protected local value file by your normal
secure process, and delete the Test State Bundle before you archive the
Development Image:

```sh
ssh LEMC_ALIAS builder images bundles delete IMAGE_UUID BUNDLE_UUID \
  --expected-revision BUNDLE_REVISION --confirm \
  --idempotency-key REQUEST_UUID --format=json
```

Remove the test App, then archive the Cookbook and Development Image. A create
request with an unknown transport result must be checked with `auth secrets
show` before retry. Reuse the same idempotency key only for the same create
payload.

## Security and cost

Cookbook YAML holds names only. Never put a value, ciphertext, key, token, or
`.env` file in this directory. Trusted recipe code receives the plaintext in
its process environment. Output redaction cannot stop encoded or transformed
disclosure by malicious code. This lesson has no external cost.

## Troubleshooting

- `missing`, `disabled`, or `undecryptable` preflight means no guest started.
  Fix the exact User Secret record; do not add a public fallback.
- `expected version` means the secret changed after you read it. Read metadata
  again before an intended update or disable action.
- A source scan that finds a value is a failure even if the value is
  disposable. Remove it from source and rotate or discard it.
- If output includes any submitted secret bytes, stop, preserve only redacted
  evidence, disable the secret, and treat the image as untrusted.

## Internal validation

Maintainers run `./scripts/validate.sh`. The default gate uses an in-process
dummy value only to check the script branch; it does not store a live secret.
