COW → ENC → format backend
CowBackend( EncBackend( LimnifsBackend | DwarfsBackend | SquashfsBackend | … ) )
ARCHITECTURE · 17
Declarative overlays.
Images are read-only forever, which is the transforms law. When a workload must write, or a slice arrives sealed, the run gains a transform: a copy-on-write layer or an encryption view. Each transform enters exclusively through a written declaration, resolved before exec and journaled at run time. Nothing is transformed that was not declared.
|
Note
|
The gated COW composite, the TEBAKO_OVERLAYS / TEBAKO_DECRYPT env grammars, vfs-deny journaling, the needs generator’s write fold, and exit 68 are shipped. The D1/D2 manifest model and the driver’s boot-time consumption are phased, and they are planned. |
One pinned stack, top to bottom
Per mount point, from the syscall side down to the bytes:
COW is always outermost, and ENC wraps exactly one image’s backend and sits
under COW. The order is not a taste: EncBackend is a decrypting read
view that implements no write path, so only COW(Enc(base)) can accept a
write, and copy-up then reads through the ENC layer; modifying a sealed
base file copies its plaintext into the operator-bound overlay store. That
store is outside the image’s confidentiality envelope, stated honestly:
scratch at rest is the host’s disk-encryption layer, never a tebako
declaration. Whiteouts name plaintext view paths, consistent with the ENC
format keeping names and structure plaintext and only regular-file content
ciphertext. A union mount composes the per-image stacks beneath COW, and
sealed union members wrap individually and merge as plaintext views.
D1 — a slice declares its writes and its seals
Two blocks extend the one needs: key of the slice manifest: the
composition grammar is amended once and owned in one place.
needs:
write: # writable areas — COW is the only mechanism
- path: /app/var/cache # absolute in-image path, inside THIS slice's tree
persistence: ephemeral # ephemeral (DEFAULT) | retained
when: [macos] # OPTIONAL platform filter
why: "mnconvert writes its font cache at boot" # MANDATORY
decrypt: # key-binding requirements — ENC
- part: /fonts/licensed # a path from identity.encryption.parts[].paths
why: "the converter reads the licensed font tree" # MANDATORY
A write area is a path that the slice will write, not a mechanism; COW is
the only mechanism, so resolution always satisfies a write need with a
CowBackend stack. persistence is the slice’s contract about its own data:
ephemeral states that any per-run scratch satisfies the slice, and
retained states that the slice’s writes must survive the run, so an
operator-bound durable store is mandatory. The operator may widen durability
upward and never demotes a retained need to scratch; declarations request,
and the operator tightens. For decrypt, the fact of encryption is owned by
the identity block (paths, algorithm, envelope refs, never keys), and the
need only names which sealed parts the workload requires opened. Sealed
state with no needs.decrypt defaults to all parts required, fail-closed.
With entries, unlisted parts may stay sealed: one artifact serves N
audiences, and each run opens exactly its declared subtree. A data slice
declares no needs.write ever, and a sealed data slice is the one exception
that may carry needs.decrypt.
D2 — the operator binds the store and the recipient
The composition document gains two top-level keys; D5 maps 1:1 as repeatable
--overlay / --decrypt flags, and flags win.
# tebako.yaml (D2)
overlays: # COW backing-store bindings
- slice: metanorma
store: ./overlays/metanorma # host dir; symbolic atoms expand
decrypt: # ENC key bindings — REFERENCES, never material
- slice: fonts-secure
recipient: pgp:3c8dba971d2b4f01 # a key in $TEBAKO_HOME/keys/
Bindings satisfy needs; they never create capability. An entry naming a
slice with no matching declared need is a named ORPHAN-BINDING error at
resolution, never a silent grant; giving an undeclared slice a write area
takes both lines, because the composition declares the need and binds the
store. There is one store per mount; the directory is created when missing
and gains a derived rw identity grant in the effective policy, so declared
write access to host surface stays visible by construction. recipient:
names a key reference, pgp: plus 16 lowercase hex, resolved against
$TEBAKO_HOME/keys/, the only secret-key home. Key material in any authored
document is a named validation error, not a grammar. The trust posture,
stated honestly, is that a package signature covers slices and the trailer,
not the bindings, and an overlay store’s content is host state, outside
every image’s trust anchor.
Resolution: before exec, never mid-run
Between the composition algorithm’s needs-union and needs-check steps, the resolver gains three ordered steps:
3a. UNION every slice's needs.write / needs.decrypt, platform-filtered,
conflict rules per declaration — the runtime's release manifest
unions identically (the env image is a mount like any other)
3b. BIND match D2 bindings to needs
write + binding → the store is checked NOW (creatable, writable)
write + NO binding → ephemeral: per-run scratch under
$TEBAKO_HOME/tmp/overlays/<run-id>/, journaled
retained: named RESOLUTION FAILURE
decrypt + binding → the key must EXIST and open an envelope
covering the part — verified at bind, never
at first read
orphan binding → named error
3c. CHECK a declared need the composition does not satisfy fails BEFORE EXEC
— a need never surfaces as a mid-run EROFS/ENOKEY
5′. EXPORT the bound set serializes to TEBAKO_OVERLAYS / TEBAKO_DECRYPT;
spawned children inherit and re-bind identically
| VAR | GRAMMAR | NOTE |
|---|---|---|
|
|
A store keeps its own = and its drive-qualified :. |
|
|
Key references cross the wire; key material never does. |
Malformed env forms fail closed with exit 68, each a named error quoting the
offending entry: an empty spec, a stray ;, a missing =, a non-absolute
mount or store, a duplicate mount, or a decrypt recipient that is not pgp:
plus 16 lowercase hex. A store containing ; is unrepresentable by
construction, because it splits into a second entry that fails the grammar.
The failure is fail-closed, never a silent misparse.
Run time: the write gate and the sealed read
A mount carrying at least one write area stacks CowBackend with the declared
area set, and the jail installs after the mounts with the derived store
grants in force. The gate’s predicate is locked: areas are absolute in-image
paths normalized at mount time; a write to an area itself or to any path
below it (on a component boundary; /a/b never covers /a/bc) is permitted;
all four write verbs (pwrite, truncate, mkdir, remove) are gated
identically; reads are never gated; and the whiteout journal keeps its EPERM
under every area set. A malformed area fails the mount with EINVAL,
fail-closed and never a silent widening.
| SURFACE | ANSWER | JOURNAL |
|---|---|---|
write under a declared write area |
It lands in the overlay store. |
|
write anywhere else in a held tree |
The answer is EROFS, the jail-safe default. |
|
read of a sealed path with no opening grant |
The answer is ENOKEY (126), never garbage. |
|
store-side IO failure (ENOSPC, EACCES) |
The answer is the write syscall’s own errno. |
No translation occurs; the run degrades like a full disk. |
Record mode discovers writes too
Under policy: record, every image mount stacks an ephemeral scratch COW,
so the "nothing is denied" promise now covers writes into held trees: the
write lands in scratch, journaled event=vfs-write, and is discarded at
exit. The payload observed a writable world that it never owned. Deny-mode
vfs-deny lines and record-mode vfs-write lines both fold into
tfs needs --from-journal, which emits a draft needs.write: block beside
needs.host:, with persistence ephemeral (the observed minimum) and each
entry carrying its why: TODO. Sealed-read denials fold into a draft
needs.decrypt: entry. The human gate is unchanged: the author flips
persistence, prunes noise, fills why, and merges into D1 or D2. The
generator never edits a manifest.
Swapping the bindings without rebuilding the slice
Overlay and key bindings live in the composition layer and are swappable by
construction. Managed mode edits tebako.yaml or passes flags. A standalone
package’s baked composition carries the resolved needs union and the
press-time bindings as the default, and the override document (argv
--compose > TEBAKO_COMPOSE > sidecar) carries overlays: / decrypt:
with the same grammar. The reserved L2 mounts[].mode: cow spelling becomes
live for exactly this, and enc stays reserved, because a sealed slice’s
fact is its own manifest’s, never the L2 row’s. The boot journal names what
ran: event=overlay mount=… store=… source=declared|ephemeral and
event=decrypt mount=… recipient=pgp:… grants=…. Key material never touches
a journal. The one added exit-code row is 68 EX_TEBAKO_OVERLAY: an
unbound retained store, missing or non-opening key material, an unwritable
store, an orphan binding, or a malformed env grammar.