# Credentials and sealing

Store configuration persists an encrypted `credential_ref` containing a source
and exact lookup name. It never persists the external secret. Named sources are
constructed from local configuration, outside all served vault directories.
An explicit reference that is missing, expired, revoked or invalid fails without
falling back to ENV, a profile file or a cloud metadata service.

## Named sources

| `kind` | Configuration | Lookup |
| --- | --- | --- |
| `environment` | No other fields | `name` is an exact environment-variable name, read on every use. |
| `files` | Absolute `directory_path` | `name` is one filename. Unix files must be regular, current-user owned, single-link, mode 0400/0600; the parent must be trusted and not group/other writable. Symlinks are rejected. |
| `systemd` | No other fields | Read the current `$CREDENTIALS_DIRECTORY/name` using the same file checks. |
| `keyring` | Public `service` name | Native macOS Security, Windows Credential Manager or Linux Secret Service entry named by `name`. Background reads never prompt or unlock a locked collection. Provision entries separately. |
| `https` | `endpoint_url`, `bearer_token_ref`, optional public-CA `certificate_path` | Authenticated HTTPS GET appends `name` as one encoded path segment. Bearer input is resolved fresh. No redirects; 3-second connection and 10-second total deadlines. |

Source responses are nonempty and limited to 256 KiB. Configuration permits at
most 64 named sources, rejects unknown dependencies and cycles, and limits
dependency depth to eight. File sources are rechecked after directory replacement;
files in a vault, including canonical aliases into one, are rejected.

For S3, a source returns JSON with `access_key_secret`, `secret_key_secret`, optional
`session_token_secret` and optional absolute Unix-second `expires_at_epoch_s`.
Unknown fields are rejected. Each credential component is at most 65536 bytes.
Password auto-unseal consumes exact UTF-8 bytes; do not append a newline. Symmetric
auto-unseal consumes exactly 32 raw bytes, with no text or base64 conversion.

Example `serve.json`, after registering the `service` wrap on the vault:

```json
{
  "secret_sources": {
    "files": {"kind": "files", "directory_path": "/run/cipherfs-secrets"}
  },
  "vaults": [{
    "name": "personal",
    "vault_path": "/srv/cipherfs/personal",
    "auto_unseal": {
      "kind": "key",
      "source_id": "service",
      "credential_ref": {"source": "files", "name": "service-key"}
    }
  }],
  "listeners": [{"listen_address": "127.0.0.1:3000"}]
}
```

Register an external key once using Core while authenticated:

```rust,no_run
let vault = cipherfs_core::Vault::open("/srv/cipherfs/personal", password)?;
let bytes = cipherfs_core::owner::read_credential_file("/run/cipherfs-secrets/service-key", 32)?;
let key: &[u8; 32] = bytes.as_slice().try_into()?;
vault.add_unlock_key_source("service", key)?;
vault.close()?;
```

Use `kind: "password"` with just `credential_ref` for a password source. The
explicit `unlock_source_ref: "env:NAME"` password shorthand also retries.
`credential_bindings` can bind an existing location and optional `store_id` to a
reference before it is saved. For persistent registration through the owner:

```sh
cipherfs store add s3 --vault /srv/cipherfs/personal --bucket example-bucket \
  --region us-east-1 --prefix team --credential-source files --credential-name s3-writer
```

The desktop S3 form and public sync API accept `credential_ref`, never access or
secret keys. Raw S3 access and secret key fields are rejected. The private native
credential-injection protocol remains restricted to the authenticated local
transport and active session, like the in-process C/Python/Go override APIs.

## Sealing and recovery

Configured serve owners acquire the lease and publish private discovery while
sealed. Source failure leaves the process alive, `/health` sealed and `/ready`
503; retry delays grow from one second to a 30-second cap. A successful source
starts a fresh session. A manual seal does not immediately auto-unseal itself.

`cipherfs seal --vault PATH` drains sessions, synchronization and owner mounts,
clears Core keys and keeps the same lease and private endpoint. `cipherfs unseal
--vault PATH` authenticates with the normal password input; `--configured` resolves
the owner's configured source over its private transport. An old file handle,
snapshot, staged write, JWT or credential context never revives after unseal.
Logout/lock still closes the owner and releases discovery.

`POST /api/v1/unseal` accepts `{"kind":"password","password_secret":"…"}` or
`{"kind":"recovery","recovery_key_secret":"…"}` as authentication proofs.
`{"kind":"configured"}` requires the private owner transport; an unauthenticated
remote caller cannot obtain a session merely by triggering a host secret lookup.
`POST /api/v1/seal` requires an active session. Wrong proof preserves an already
valid session. Core operations return `Sealed`; the native error code is 32 and
the HTTP classification is `SEALED` (503).

`SyncedVault::set_idle_timeout(Some(Duration))` seals after user inactivity;
background polling is not activity. `None` disables it. Unseal takes an explicit
fresh provider and synchronization must be enabled again. The blocking
`Client::set_idle_timeout` and bindings use optional milliseconds. Their `unseal`
and `unseal_with_unlock_key` start without a provider fallback; configure fresh
sources or credentials afterwards. `open_sealed`, independent key-source
registration/removal, and source configuration are also available to bindings.

Password, recovery and symmetric wraps coexist in the `unlock_wraps` table of
metadata schema 1; CAS is schema 1 and shared format 1 is unchanged. See
`FORMAT.md` section 5.5 for framing.

Retained native vault keys and S3/WebDAV credential buffers occupy independently
owned locked pages; locking failure is an error. They are zeroized before pages
are unlocked. Linux also disables dumpability and sets both core-dump limits to
zero; macOS disables core dumps and locks pages. These are one-way process-wide
restrictions, including in embedders. Temporary cryptographic/vendor/HTTP copies
and caller-owned buffers cannot all be wiped or locked by the library. An active
provider supplied by the application remains responsible for its own secret
storage. No implementation protects a running process from root or its VM host.
Source-specific threat boundaries are listed in the README.

## Writer permissions

Use a separate S3 writer role without deletion rights. Replace the principal,
bucket and `team/` prefix in this bucket-policy template:

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {"Effect":"Allow","Principal":{"AWS":"arn:aws:iam::123456789012:role/CipherFSWriter"},"Action":"s3:ListBucket","Resource":"arn:aws:s3:::example-bucket","Condition":{"StringLike":{"s3:prefix":["team/","team/*"]}}},
    {"Effect":"Allow","Principal":{"AWS":"arn:aws:iam::123456789012:role/CipherFSWriter"},"Action":["s3:GetObject","s3:PutObject"],"Resource":"arn:aws:s3:::example-bucket/team/*"},
    {"Effect":"Deny","Principal":{"AWS":"arn:aws:iam::123456789012:role/CipherFSWriter"},"Action":["s3:DeleteObject","s3:DeleteObjectVersion"],"Resource":"arn:aws:s3:::example-bucket/team/*"},
    {"Effect":"Deny","Principal":{"AWS":"arn:aws:iam::123456789012:role/CipherFSWriter"},"Action":["s3:PutLifecycleConfiguration","s3:PutBucketPolicy","s3:DeleteBucketPolicy","s3:PutBucketVersioning","s3:DeleteBucket"],"Resource":"arn:aws:s3:::example-bucket"}
  ]
}
```

Keep lifecycle expiration and administrator/GC identities under separate control.
Delete denial does not prevent overwrites by an otherwise authorized writer.
This follows AWS's [bucket-policy examples](https://docs.aws.amazon.com/AmazonS3/latest/userguide/example-bucket-policies.html)
and [operation permission mapping](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-with-s3-policy-actions.html).

`cipherfs store check-permissions --vault PATH --id ID` checks current credentials
with a bounded LIST, confirms a cryptographically random probe key is absent,
then tries DELETE on that absent key. It performs no PUT and never targets a
known application object. Only an authenticated `AccessDenied` response followed
by another successful LIST reports `{"delete_denied":true}`; expired/revoked
credentials and unrelated failures are errors. Allowed deletion reports false
and a failing CLI exit status. This observes one namespace, not an account-wide
policy or version-deletion proof. On versioned buckets an allowed probe can
create a [delete marker](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObject.html).
