CipherFS
In development · unreleased · format may change

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

kindConfigurationLookup
environmentNo other fieldsname is an exact environment-variable name, read on every use.
filesAbsolute directory_pathname 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.
systemdNo other fieldsRead the current $CREDENTIALS_DIRECTORY/name using the same file checks.
keyringPublic service nameNative 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.
httpsendpoint_url, bearer_token_ref, optional public-CA certificate_pathAuthenticated 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.

Source file in the repository