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:
{
"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:
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:
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:
{
"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 and operation permission mapping.
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.