enc
Recipe card from the charly-automation plugin (Commands — runtime CLI verbs).
Enc - Encrypted Storage
Section titled “Enc - Encrypted Storage”Overview
Section titled “Overview”Encrypted volume backing is configured at deploy time via charly config --encrypt <volume>. Gocryptfs-encrypted volumes store sensitive data (credentials, keys, configs) with transparent encryption at rest. The cipher directory lives on disk; the plain directory is mounted on demand. charly config <image> handles initialization and mounting during deployment setup. charly start mounts encrypted volumes inline before starting the container.
Quick Reference
Section titled “Quick Reference”| Action | Command | Description |
|---|---|---|
| Configure encrypted | charly config <image> --encrypt <vol> |
Set volume backing to encrypted |
| Setup (init + mount) | charly config <image> |
Initialize cipher dirs and mount encrypted volumes |
| Mount | charly config mount <image> |
Mount encrypted volumes |
| Unmount | charly config unmount <image> |
Unmount encrypted volumes |
| Status | charly config status <image> |
Show mount status |
| Change password | charly config passwd <image> |
Change encryption password |
charly config mount and charly config unmount accept --volume NAME to target a specific volume (otherwise all encrypted volumes are affected). charly config status and charly config passwd do NOT — they take only the deployment (and -i/--instance), and passing --volume to them fails with unknown flag --volume.
Configuration
Section titled “Configuration”Encrypted volumes are configured at deploy time, not build time. Use charly config --encrypt to set a candy-declared volume’s backing to encrypted:
# Configure "library" volume as encryptedcharly config immich --encrypt library
# Or via canonical syntaxcharly config immich -v library:encrypted
# Or via env varCHARLY_VOLUMES_IMMICH="library:encrypted" charly config immich --password autoThis saves to charly.yml:
volumes: - name: library type: encryptedRules:
- Volume must be declared in
charly.yml(or provided as deploy-only withpath:) name: matches a candy volume name, must match^[a-z0-9]+(-[a-z0-9]+)*$
Per-Volume Explicit Path
Section titled “Per-Volume Explicit Path”Each encrypted volume can specify its own storage directory:
# Explicit per-volume paths (each volume gets its own directory)charly config immich-ml \ --volume library:encrypt:/mnt/nas/immich/library \ --volume pgdata:encrypt:/mnt/nas/immich/pgdata
# Canonical syntaxcharly config immich-ml -v library:encrypted:/mnt/nas/immich/libraryThe path is the direct volume directory — cipher/ and plain/ are created inside it:
/mnt/nas/immich/library/ cipher/ # gocryptfs encrypted data plain/ # FUSE mount pointWithout an explicit path, the global encrypted_storage_path is used with an charly-<image>-<name> prefix (backward compatible).
Storage Layout
Section titled “Storage Layout”Default (no explicit path)
Section titled “Default (no explicit path)”~/.local/share/charly/encrypted/ charly-<image>-<name>/ cipher/ # Encrypted data (always on disk) plain/ # Decrypted mount point (mounted on demand)Override base path: charly settings set encrypted_storage_path /path/to/storage or CHARLY_ENCRYPTED_STORAGE_PATH=/path.
Commands
Section titled “Commands”Setup (charly config)
Section titled “Setup (charly config)”charly config my-app --encrypt secrets # Set volume as encrypted + init + mountcharly config my-app --password auto # Auto-generate passwordcharly config my-app --password manual # Prompt for passwordcharly config <image> handles both initialization (creating cipher directories) and mounting in a single step. If volumes are already initialized, it mounts them. Password is cached in kernel keyring for multi-volume images.
charly config mount my-app # Mount all encrypted volumescharly config mount my-app --volume secrets # Mount specific volumePrompts for password (or reuses from keyring). Each volume is mounted inside a transient systemd scope unit (charly-enc-<image>-<volume>.scope) via systemd-run --scope --user. The plain directory becomes available for container use. Scope units can be listed with systemctl --user list-units 'charly-enc-*'.
Unmount
Section titled “Unmount”charly config unmount my-app # Unmount allcharly config unmount my-app --volume secrets # Unmount specificUnmount calls fusermount3 -u then stops the scope unit (systemctl --user stop charly-enc-<image>-<volume>.scope) to clean up the gocryptfs daemon.
Status
Section titled “Status”charly config status my-app# secrets: mounted# configs: not mountedChange Password
Section titled “Change Password”charly config passwd my-appChanges the gocryptfs password for all encrypted volumes of an image.
Single Password
Section titled “Single Password”When an image has multiple encrypted volumes, charly config, charly config mount, and charly start all use systemd-ask-password --id=charly-<image> to cache the passphrase in the kernel keyring. Password is prompted once and reused for all volumes.
Credential lookup — iteration-capable ssClient
Section titled “Credential lookup — iteration-capable ssClient”Why iteration matters: a Secret Service client that looks up credentials
via the default alias only (no iteration) fails entirely when that alias
resolves to a broken or stub collection — commonly seen with KeePassXC’s
FdoSecrets plugin when a previously-exposed database is unloaded. Such a client
would hang charly config mount forever polling for a keyring that can’t serve the
secret, and a quadlet unit with TimeoutStartSec=0 would wedge in
activating (start-pre) indefinitely.
The credential store is EXTERNALIZED into the out-of-process candy/plugin-secrets
plugin (the C2 dep-shed; go-keyring no longer links into charly’s core). The plugin
ships its own minimal godbus-based Secret Service client
(candy/plugin-secrets/secret_service.go, the ssClient type) with a three-step
iteration in findItemAcrossCollections:
- Try the collection at the
defaultalias (if healthy — skipped if its property reads error out). - Try a collection matching the
keyring_collection_labelsetting (if non-empty — see/charly-build:settings). Useful for pinning a specific collection by label in multi-database setups. - Try every other healthy collection in listing order.
Broken collections are skipped with a diagnostic line to stderr:
charly: skipping broken Secret Service collection <path>: <error>charly: Secret Service default alias target <path> is unhealthy; falling back to collection iterationThe client returns ErrSSNotFound if no collection has the credential, and
ErrSSAllBroken if every candidate errored on unlock or search (distinct
from “credential simply not stored”).
Call path:
KeyringStore.Get → keyringGetViaSSClient → newSSClient() → ssClient.findItemAnyCollection(service, username, preferLabel) → readAlias("default") → health check → candidate → collections() → filter by preferLabel → candidate → collections() → filter healthy, dedup vs already-tried → candidates → for each candidate: unlock → SearchItems({service, username}) → first match wins → ssClient.getSecret(item) → []byteSource: candy/plugin-secrets/secret_service.go (ssClient + findItemAcrossCollections),
candy/plugin-secrets/credential_keyring.go (the KeyringStore.Get entry point that
delegates to ssClient). Covered by the unit tests in
candy/plugin-secrets/secret_service_test.go including default-alias-healthy,
default-alias-broken-fallback-to-iteration, preferLabel routing, all-broken
→ ErrSSAllBroken, not-found-anywhere → ErrSSNotFound, search/unlock errors,
and candidate dedup. charly’s core reaches them over verb:credential (the
charly/credential_plugin.go adapter).
Credential source semantics
Section titled “Credential source semantics”ResolveCredential (the core adapter charly/credential_plugin.go; the env check + the
env-less store chain resolveStoreChain in candy/plugin-secrets/store.go) returns a
(value, source) pair where the source string is one of:
| Source | Meaning | Caller reaction |
|---|---|---|
env |
Resolved from an env var override (e.g. GOCRYPTFS_PASSWORD) |
Terminal: use the value |
keyring |
Found in the system keyring via the iteration-capable read path | Terminal: use the value |
config |
Found in ~/.config/charly/config.yml (fallback or explicit backend) |
Terminal: use the value |
locked |
Primary backend is present but locked (e.g. keyring not yet unlocked after login) | Wait for the unlock — unbounded and event-driven under systemd |
unavailable |
Primary backend probe failed (e.g. ssClient saw every collection error out); fell back to ConfigFileStore but the credential isn’t stored there either |
Retry on a fixed 5s poll (EncMountPollPeriod) — may be transient at early boot |
default |
Backend queried successfully and the credential is not stored anywhere | Terminal — prompt the user interactively or fail with remediation |
The critical distinction is between default and unavailable: both
look identical from the ConfigFileStore return value (empty string, nil
error), but they have opposite recovery semantics. Conflating them — polling
forever on both — would wedge charly-<image>.service under TimeoutStartSec=0
whenever the keyring is broken, so the two are kept distinct.
Under systemd (INVOCATION_ID set, typical for ExecStartPre):
-
default→ fail immediately with the actionable error built byEncNotStoredError:encryption passphrase not available for charly/enc/<image>(backend=<backend>, source=<source>). Remediation: run `charly doctor` tocheck keyring health, store with `charly secrets set charly/enc <image>`,or switch backend with `charly settings set secret_backend config` -
locked→ wait, event-driven and unbounded, whenever the caller wired an unlock waiter — which the systemd path always does (see “Event-driven keyring waiting” below). This path has no deadline. With no waiter wired it degrades to the bounded retry below. -
unavailable→ retry up toEncMountDeadline(package variable insdk/deploykit/enc_passphrase.go, default2 * time.Minute, poll periodEncMountPollPeriod = 5 * time.Second). After the deadline elapses, fail with a diagnostic listing backend, source, and remediation. -
env/keyring/configwith a non-empty value → return immediately.
In interactive mode (no INVOCATION_ID), ResolveEncPassphraseForMount
delegates to ResolveEncPassphrase, whose last resort is AskPassword
(sdk/deploykit/enc_probe.go) — a systemd-ask-password prompt.
Covered by the table tests in sdk/deploykit/enc_passphrase_test.go (11 test
functions, including default fails fast, locked retries then fails,
locked then succeeds, the three waiter outcomes — value, cancelled context,
and default — unavailable stays bounded, success returns immediately, and
reset is called between retries).
Troubleshooting: broken Secret Service collection
Section titled “Troubleshooting: broken Secret Service collection”Symptom: charly config mount <image> hangs, or a service’s ExecStartPre
phase blocks in activating (start-pre) state. journalctl --user -u charly-<image>.service shows lines like:
charly-<image>[...]: charly: Secret Service default alias target <path> is unhealthy; falling back to collection iterationOr charly doctor reports:
[!] Secret Service collections -- N healthy + 1 broken. Broken: /org/freedesktop/secrets/collection/<path>. Healthy: "<label>"[!] Keyring index consistency -- <N indexed, <M> missing: charly/enc/<image>...Cause: KeePassXC’s FdoSecrets plugin can advertise a stub collection
(commonly aliased as default) whose every DBus method call returns
org.freedesktop.Secret.Error.NoSuchObject or an Input/output error. The
real credentials are in a sibling collection with a different label. This
pattern has been observed when a KeePassXC database was previously exposed
via FdoSecrets but later unloaded or renamed, while the collection entry
lingers in KeePassXC’s internal state.
Fix (nothing to do on the charly side): ssClient iterates past the broken
collection automatically and finds the credential in the healthy sibling.
The credential lookup just works. No configuration change needed.
Optional cleanup (KeePassXC side):
- Open KeePassXC
- Tools → Settings → Secret Service Integration → Exposed Databases
- Review the list; remove stale entries
- Restart KeePassXC
- Re-run
charly doctor— the “Secret Service collections” check should now report “N healthy collection(s)” with no broken count.
Pinning a preferred collection: if your setup has multiple healthy
collections and you want charly to prefer one by label (e.g. to avoid
iteration overhead), set:
charly settings set keyring_collection_label "<collection-label>"findItemAnyCollection will try that label-matched collection after the
default alias (if healthy) and before untargeted iteration. Environment
override: CHARLY_KEYRING_COLLECTION_LABEL. See /charly-build:settings for the full
runtime-config interface.
Diagnostic direct query (busctl): to check a specific collection by path without using charly:
# List collectionsbusctl --user call org.freedesktop.secrets /org/freedesktop/secrets \ org.freedesktop.DBus.Properties Get ss \ org.freedesktop.Secret.Service Collections
# Read the default alias targetbusctl --user call org.freedesktop.secrets /org/freedesktop/secrets \ org.freedesktop.Secret.Service ReadAlias s default
# Probe a collection's health (should return Label without error)busctl --user call org.freedesktop.secrets <collection-path> \ org.freedesktop.DBus.Properties Get ss \ org.freedesktop.Secret.Collection LabelA healthy collection returns its label; a broken stub returns an I/O error.
Storing the gocryptfs passphrase
Section titled “Storing the gocryptfs passphrase”Store an encrypted-volume passphrase explicitly in the active credential store (Secret Service when available, config-file fallback otherwise):
charly secrets set charly/enc my-app <passphrase>To serve credentials from an existing KeePass database, open it in KeePassXC and
enable the FdoSecrets plugin so its entries appear on the Secret Service bus —
charly’s keyring backend reads them like any other collection. See /charly-build:secrets
for the full credential-store chain (env → Secret Service keyring → config-file
fallback).
Scope Unit Architecture
Section titled “Scope Unit Architecture”The gocryptfs / systemd-run --scope / fusermount3 SHELLING runs OUT of charly’s
core: it is served by the compiled-in candy/plugin-enc (verb:enc, C16a). The enc
leaf in candy/plugin-pod prelifts the resolved per-volume plan + passphrase and
Invokes the plugin’s OpExecute; the plugin runs the commands. The runtime behavior
below is unchanged by that extraction.
Each encrypted volume is mounted via systemd-run --scope --user --unit=charly-enc-<image>-<volume> -- gocryptfs -allow_other <cipherdir> <plaindir>. This creates a transient systemd scope unit that:
- Survives container stop/restart — scope units are independent of the container service’s cgroup, so
KillMode=mixedon service stop does not kill gocryptfs - Keeps mounts browsable on host — the host user can access
plain/directories even when the container is stopped - Handles stale scopes — if a scope persists from a crash, the next mount attempt stops the stale scope and retries
- Cleans up on unmount —
charly config unmountcallsfusermount3 -uthensystemctl --user stop charly-enc-<image>-<volume>.scope. The same teardown is available as a one-shot from the stop verb viacharly stop <image> --unmount(see/charly-core:stop); plaincharly stopdeliberately leaves scopes running because the next start fast-paths through thecharly config mountshort-circuit.
Scope unit naming: charly-enc-<image>-<volume>.scope (e.g., charly-enc-immich-ml-library.scope).
List active scopes: systemctl --user list-units 'charly-enc-*'
Why -allow_other is Required
Section titled “Why -allow_other is Required”Rootless podman with --userns=keep-id creates a two-level user namespace. During container mount setup, crun runs as inner uid 0, which maps through the namespace chain to host uid 524288 (a subordinate uid), not the FUSE mount owner (uid 1000). FUSE’s kernel check rejects access. The -allow_other flag bypasses this check, allowing crun to bind-mount from the FUSE filesystem. gocryptfs auto-enables default_permissions with -allow_other, so kernel UNIX permission checks still apply (0700 dirs restrict access to the mount owner). Confirmed by podman issues #14488, #15314, #16350, #25894.
Host prerequisite: user_allow_other in /etc/fuse.conf
Section titled “Host prerequisite: user_allow_other in /etc/fuse.conf”Because every mount uses -allow_other, the host’s /etc/fuse.conf MUST contain an active
user_allow_other line — fusermount3 refuses -allow_other for a non-root user without it
(option allow_other only allowed if 'user_allow_other' is set). charly handles this proactively:
charly doctor’s Encrypted Storage group checks it (checkFuseAllowOther, WARNING + fix hint
when missing), and the enc leaf preflights it before a mount/ensure op
(FuseAllowOtherEnabled in sdk/deploykit/enc_probe.go) — failing fast with the exact fix
(echo user_allow_other | sudo tee -a /etc/fuse.conf) instead of the raw fusermount3 error
mid-mount. Enable it once per host that runs charly encrypted volumes.
Integration with Runtime
Section titled “Integration with Runtime”charly shell/charly start(direct mode): resolves volume backing from charly.yml, verifies encrypted volumes are mounted, appends-v <plain>:<container-path>flags.charly startmounts encrypted volumes inline via systemd-run scopes before starting the containercharly config(quadlet mode): generates quadlet file withExecStartPre=charly config mount <image>for encrypted services. ExecStartPre creates scope units internally — these are independent of the container service. Whether that unit is started at boot depends on the configured secret backend (see below)charly remove --purge: removes named volumes- Data provisioning:
charly config --seed(default) provisions data from data candies into bind-backed directories after mounting encrypted volumes. Works for both bind and encrypted volume types
Boot Behavior: Which Backend, and Why
Section titled “Boot Behavior: Which Backend, and Why”An encrypted deploy’s quadlet carries ExecStartPre=charly config mount <image>.
Whether systemd starts that unit at boot is decided by which secret backend is
configured — nothing else:
secret_backend |
what the quadlet gets | at boot |
|---|---|---|
keyring, auto, unset |
[Install] WantedBy=default.target, and [Service] TimeoutStartSec=0 |
starts; ExecStartPre blocks in the mount until the keyring unlocks, rather than timing out |
config |
no [Install] WantedBy= target |
does not start; needs an explicit charly start <image> |
auto is the default and resolves to the keyring, so the first row is the
out-of-the-box arrangement with nothing to configure.
The gate is the backend name, and the reason is policy rather than capability.
emitInstallSection (sdk/deploykit/quadlet.go) writes WantedBy=default.target
unless EncryptedMounts && !KeyringBackend, and cfg.KeyringBackend is fed by
secretBackendIsKeyring() (candy/plugin-deploy-pod/secrets_resolve.go), which
tests membership of {"keyring", "auto", ""}. That is the branch to read if you
want to know why a deploy skipped boot.
It is worth being exact about why, because the obvious reading is wrong.
The config backend does not need a person: ConfigFileStore.Get
(candy/plugin-secrets/credential_config.go) reads ~/.config/charly/config.yml
with no prompt and no keyring — strictly less human-dependent than Secret
Service, which waits for a session unlock. So a rule phrased as “start it at boot
if the passphrase can be obtained unattended” would grant autostart to the
cleartext case and deny it to the encrypted one.
The exclusion is a security decision, and it is recorded: sdk#121 ruled that
secret_backend: config stays supported with a warning whenever a passphrase is
stored in cleartext — a warning that then shipped as code in charly#213
(warnCleartextStorage) — and rejected the capability form on the grounds that
“it would grant autostart to a deploy whose passphrase sits in cleartext”. A
passphrase in a plaintext file must not be auto-mounted, unattended, at every
boot. Secret Service qualifies because the key is protected until an operator
unlocks it, not because it is easier to obtain.
One wrinkle worth knowing before you trust the source: the only rationale the
code carries in-line is the comment emitInstallSection emits for the excluded
case — # Encrypted volumes require 'charly start' (no keyring auto-unlock) —
which states the capability reading refuted above. The comment is stale; the
ruling is the authority.
The mount-time predicate usesWaitingBackend
(ResolveEncPassphraseForMountWithResolver, sdk/deploykit/enc_passphrase.go)
tests the same set, {"keyring", "auto", ""} — but answers a different
question: will the resolver wait rather than fail fast? Same members, different
meaning, and deduplicating them would merge an autostart policy with a retry
policy. See task #27.
Flow on reboot, unattended case:
- Boot → systemd starts user instance (linger) → quadlet service starts
- ExecStartPre →
charly config mount→ keyring locked → the enc leaf RPCsverb:credential await-unlockto candy/plugin-secrets, which subscribes to DBusPropertiesChangedsignals on Secret Service collections, with a 30-second backstop re-probe (awaitSignalBackstop) - User logs in → PAM unlocks GNOME Keyring / KeePassXC unlocks database
- DBus signal fires (or backstop re-probes) → passphrase found → volumes mount → container starts
- The wait is unbounded — charly blocks until the keyring unlocks or
systemd sends SIGTERM on
systemctl stop. No arbitrary deadline.
Event-driven keyring waiting: when
source=locked under a waiting-capable backend, the waiter injected into
ResolveEncPassphraseForMount RPCs verb:credential await-unlock to
candy/plugin-secrets — the Secret Service owner since the godbus dep-shed
(charly’s core links no godbus). The plugin subscribes to DBus
org.freedesktop.DBus.Properties.PropertiesChanged signals on the
/org/freedesktop/secrets/collection/* namespace; its wait loop blocks
on select { case <-sigCh | case <-backstop | case <-ctx.Done() } —
zero CPU cost between events. No polling. The blocking gRPC Invoke survives
an unbounded wait (go-plugin sets no keepalive/idle timeout on the local
Unix-socket connection).
awaitSignalBackstop— safety-net re-probe interval (default30 * time.Second). Catches unlock events when the Secret Service provider does not emitPropertiesChanged(KeePassXC’s FdoSecrets plugin does NOT emit this signal; GNOME Keyring and KDE Wallet do). The backstop is what catches the unlock on KeePassXC hosts.awaitProgressLogInterval— throttle for periodic “still waiting” journal output (default1 * time.Hour).- SIGTERM cancellation:
ResolveEncPassphraseForMountWithResolverbuilds the wait ctx viasignal.NotifyContext(…, SIGINT, SIGTERM)and passes it on the Invoke —systemctl stopsends SIGTERM, the ctx cancels, gRPC propagates the cancellation to the plugin’s Invoke, the loop returns cleanly, and systemd transitions the unit toinactive. - If the DBus session bus is unavailable (edge case: linger-based start
before graphical session), the plugin falls back to backstop-only
polling at the same
awaitSignalBackstopcadence — still unbounded, still low-resource.
Source: candy/plugin-secrets/keyring_unlock_wait.go (awaitUnlock,
awaitUnlockLoop, awaitUnlockBackstopOnly, isCollectionUnlockedSignal),
reached via verb:credential await-unlock. The waiter is injected by the caller:
candy/plugin-pod/enc_cmd.go (pluginAwaitKeyringUnlock) passes it into
deploykit.ResolveEncPassphraseForMount. Core’s former
pluginCredentialStore.awaitUnlock/credentialAwaiter seam
(charly/credential_plugin.go) is DELETED (K-wave 2 cone CONTESTED — zero
production callers; the pod enc path carries its own waiter).
Bounded retry for source=unavailable: transient
backend-probe failures (source=unavailable) go through RetryUnavailable,
a bounded poll loop with two package-level variables in
sdk/deploykit/enc_passphrase.go:
EncMountDeadline— total wall-clock cap (default2 * time.Minute)EncMountPollPeriod— interval between probes (default5 * time.Second)
source=default (credential not stored anywhere) is terminal and fails
immediately with an actionable error — no amount of retrying will conjure
a credential that was never stored.
Crash recovery: FUSE mounts survive container restarts because scope
units are independent of the container service cgroup. On restart, the
ExecStartPre=charly config mount step hits the fast-path short-circuit:
All encrypted volumes for <image> already mounted (N/N)When every requested volume is already mounted, pluginEncMount
(candy/plugin-pod/enc_cmd.go) iterates the mount list once, finds every target
is live, and returns nil without calling ResolveEncPassphraseForMount,
touching the credential store, or dispatching verb:enc at all. This means a broken keyring
backend does NOT block restarts of running services — only fresh mounts
(e.g., after reboot) need the keyring. If gocryptfs crashes (scope dies),
the next charly config mount or charly start detects the stale scope, stops
it, and remounts fresh — at which point the iteration-capable ssClient
kicks in.
Pre-start safety check: cipher populated + plain empty
Section titled “Pre-start safety check: cipher populated + plain empty”VerifyBindMounts (sdk/deploykit/enc_probe.go) runs in the charly start / charly shell direct-mode code path before the container is started. For any type: encrypted volume that does not show up as a FUSE mount, an extra discrimination fires before the generic “not mounted” error: when the cipher dir on disk holds user data (anything beyond the gocryptfs.conf + gocryptfs.diriv metadata files) AND the plain mount target is empty, the error switches to a louder form spelling out the data-loss risk:
encrypted volume "library": cipher dir at /home/.../charly-immich-library/cipher is populated but plain mount at /home/.../charly-immich-library/plain is empty — refusing to start (would write plaintext over encrypted data); run 'charly config mount immich' firstThis guards against a real data-loss shape: a quadlet missing the ExecStartPre=charly config mount <image> auto-mount hook (see “Boot Behavior: Which Backend, and Why” above and /charly-build:migrate “charly migrate”) would silently bind an empty plain/ over a populated cipher tree, the container’s services would initdb / first-run-wizard against the empty dir, and plaintext data would accumulate on top of an encrypted vault. This error class fails the start IMMEDIATELY when charly start detects that exact pre-start state.
Important caveat on quadlet-managed services. This check runs only in the direct-mode (CLI) path. systemd-managed quadlet services bypass it — they go straight to podman after ExecStartPre=charly config mount <image> succeeds. The actual root-cause fix for those is the ExecStartPre hook itself; VerifyBindMounts is a belt-and-suspenders safety net for the direct path.
Helper: CipherPopulatedPlainEmpty(cipherDir, plainDir) returns true only when both conditions hold. Returns false on any os.ReadDir error (the surrounding error path will surface those — this helper is purely a discrimination hint). Source: sdk/deploykit/enc_probe.go. Tested by sdk/deploykit/enc_probe_test.go:TestCipherPopulatedPlainEmpty (5 sub-cases: dangerous, metadata-only, plain-non-empty, missing-cipher, missing-plain).
Volume Backing Override
Section titled “Volume Backing Override”When a volume is configured as type: encrypted in charly.yml, it overrides the default named volume. The Docker/Podman named volume is not created – the gocryptfs mount is used instead.
# charly.yml declares a volume:volumes: - name: data path: "~/.myapp"
# charly.yml configures it as encrypted:volumes: - name: data type: encryptedPlain (Non-Encrypted) Bind Mounts
Section titled “Plain (Non-Encrypted) Bind Mounts”For comparison, plain bind mounts use type: bind:
charly config my-app --bind data=/mnt/nas/data # Explicit host pathcharly config my-app --bind data # Auto path: ~/.local/share/charly/volumes/my-app/dataPlain bind mounts do not use encrypted storage commands. They are direct host directory mounts.
Source files:
sdk/deploykit/enc_probe.go— the enc PROBE + PLAN surface, kit-resident so both the CLI leaves and a plugin caller share one implementation.EncPlanFor/EncPlanForConfigbuild the per-volume plan (resolved cipher/plain dirs + init/mounted flags + scope-unit name);LoadEncryptedVolumeloads the encrypted-volume set from the per-host config;EncStatusis a pure probe+print; the path/probe helpersResolveEncVolumeDir/IsEncryptedMounted/IsEncryptedInitialized/CipherPopulatedPlainEmptybackVerifyBindMounts;FuseAllowOtherEnabledis the/etc/fuse.confpreflight;AskPasswordis thesystemd-ask-passwordpromptsdk/deploykit/enc_passphrase.go— the passphrase-resolution ORCHESTRATION, taking its credential store as an injectedCredentialAccessrather than by name.ResolveEncPassphrase(env var → store → auto-generate or prompt),ResolveEncPassphraseForMount+ its testable…WithResolvercore (theusesWaitingBackendsplit),EncNotStoredError,RetryUnavailable(the boundedsource=unavailablepoll) and its two knobsEncMountDeadline/EncMountPollPeriodcandy/plugin-pod/enc_cmd.go— the enc SHIM + deploy-model (C16a, relocated from the DELETEDcharly/enc.go, K-wave 2).pluginEncMount/pluginEncUnmount/encPasswd/ensureEncryptedMountsare thin shims that HOST-PRELIFT the per-volume plan (encPlanFor: resolved cipher/plain dirs + init/mounted flags + scope-unit name) and the passphrase, thenencExecViaPluginresolves verb:enc and Invokes OpExecute. Keeps (deploy-model):encMount’s all-mounted short-circuit,encStatus(pure probe+print), the path/probe helpers (encryptedPlainDir/isEncryptedMounted/isEncryptedInitialized/cipherPopulatedPlainEmpty— the mandatorily-coreResolveVolumeBacking+verifyBindMountsconsume them),loadEncryptedVolume(loader),resolveEncPassphraseForMount(bounded retry forsource=unavailableviaretryUnavailable),awaitKeyringUnlockViaPlugin(thesource=lockedwaiter — delegates the event-driven DBus wait toverb:credential await-unlock, out-of-process in candy/plugin-secrets, so charly’s core links no godbus)candy/plugin-enc/enc.go— the ENCRYPTED-VOLUME (gocryptfs) MECHANICS plugin (C16a, verb:enc, compiled-in): the gocryptfs /systemd-run --scope --unit=charly-enc-<dir>-<volume>/ fusermount3 /gocryptfs -init/gocryptfs -passwd/ extpass SHELLING (mountVolumes/unmountVolumes/ensureVolumes/passwdVolumes/runGocryptfsScope/encExtpassArgs), driven by the host-preliftedspec.EncExecInput.-allow_otherfor rootless keep-id + the stale-scope retry live here. Wire types: CUE-sourced atspec/schema/enc.cue, generated intospec/spec/cue_types_gen.go(#EncExecInput/#EncVolumePlan/#EncExecReply, shared by the shim + the plugin); the plainEncMethod*string-selector constants stay hand-written inspec/spec/enc_consts.go(never a JSON/YAML shape forgengotypesto generate)candy/plugin-secrets/keyring_unlock_wait.go—awaitUnlock(the externalized event-driven DBus signal wait forsource=locked),awaitUnlockLoop,awaitUnlockBackstopOnly,isCollectionUnlockedSignal(the collection-unlocked signal filter),awaitSignalBackstop(30s),awaitProgressLogInterval(1h)charly/credential_plugin.go— the CORE adapter (THINned 213→109, K-wave 2 cone CONTESTED):DefaultCredentialStore(→ thecredentialResolverinterface),ResolveCredential(the"unavailable"-vs-"default"source distinction),pluginCredentialStore.call/callCtx/resolve(the VNC-password resolve path). The formerpluginCredentialStore.awaitUnlock+credentialAwaiterinterface +resolveSecretBackend/resetDefaultCredentialStore/CredentialStoreinterface are DELETED (zero production callers — the keyring-unlock wait lives plugin-side in candy/plugin-pod’spluginAwaitKeyringUnlock, driven viadeploykit.ResolveEncPassphraseForMount).candy/plugin-secrets/secret_service.go— godbus-based ssClient,findItemAcrossCollections(with locked-vs-broken tracking),ssOpsinterface for test injection,ErrSSNotFound/ErrSSAllBroken/ErrSSInteractiveUnlockRequiredsentinel errorscandy/plugin-secrets/credential_keyring.go—KeyringStore.Probe(iterates collections, accepts if ≥1 healthy),KeyringStore.Get(delegates tokeyringGetViaSSClient, mapsErrSSInteractiveUnlockRequiredtoKeyringLockedError), index-divergence warningcandy/plugin-secrets/store.go—DefaultCredentialStore(tracksdefaultStoreProbeErr),resolveStoreChain(the env-less store resolution the core adapter’sResolveCredentialforwards to oververb:credential)sdk/deploykit/deploy_volume.go—ResolveVolumeBacking, which splits a box’s declared volumes into named volumes and bind-backed mounts. The volume type itself is schema-sourced asspec.DeployVolume(spec/schema/deploy.cue#DeployVolume)candy/plugin-secrets/config_store.go— theKeyringCollectionLabelfield (thekeyring_collection_labelsetting)
Cross-References
Section titled “Cross-References”/charly-core:deploy– Quadlet integration, volume backing configuration, charly.yml/charly-core:charly-config–encrypted_storage_pathandvolumes_pathsettings,charly config mountshort-circuit fast-path documented there too/charly-core:service– Container lifecycle,charly startinline mount/charly-build:secrets– Credential store hierarchy (env → keyring → config),charly secrets set charly/enc <image>to store a gocryptfs passphrase explicitly,charly secrets listto inspect indexed keys/charly-build:settings–secret_backend,keyring_collection_label,encrypted_storage_path, and other runtime config keys that control credential + volume resolution/charly-core:charly-doctor– “Secret Service collections” health check, “Keyring index consistency” cross-check; invokecharly doctorwhen diagnosing broken-collection symptoms
When to Use This Skill
Section titled “When to Use This Skill”MUST be invoked when the task involves encrypted storage, gocryptfs, or encrypted volume backing. Invoke this skill BEFORE reading source code or launching Explore agents.
Workflow position: Pre-deployment. Configure encrypted volumes during charly config. See also /charly-core:deploy (volume backing).