docs: lead with folder purpose; surface RBAC + WORM on federal page
All checks were successful
Deploy content to live site / deploy (push) Successful in 3s

reference.html § 9: rewrite the canonical-folder tree so each line leads
with what the folder is FOR (drafting space, "about to issue" lane,
permanent record per counterparty, planned deliverables list, review
queue) rather than mechanics. The lifecycle stage of a document is now
visible from its location alone. Mechanics (lazy creation, case-fold
matching, virtual user home, paired delete on issue) demoted to a
single trailing paragraph so a reader can grasp the layout without
needing to track them.

federal.html: surface the access-control features that landed since the
page was written —

- Role-based access control as a first-class shipped feature, with the
  AC-2 / AC-3(7) mapping called out.
- Verb-based least privilege (r/w/c/d/a) under AC-6, with the rc
  shape used by immutable archives flagged explicitly.
- WORM enforcement on archive/<party>/{received,issued}/ under AU-9
  and MP-5, including the at-the-WORM-folder grant pattern that lets
  doc controllers drop transmittals without giving them overwrite.
- Cascade tracer (/.profile/effective-policy) under AC-3 reviewability.
- OPA wire-format detail (input shape + cache TTL + fail-open).

Move "Role-based access control" out of the "what you'd add for ATO"
table now that it's shipped; replace with "Identity-provider role
sync" — the integrator's job is wiring AD/Okta/EntraID groups into
the existing role members: list, not building RBAC from scratch.
Update "Policy export" to acknowledge the per-path tracer that already
ships and frames the missing piece as the batch-export companion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-05-07 09:42:45 -05:00
parent f81fb4e769
commit 6aae181d19
2 changed files with 51 additions and 36 deletions

View file

@ -54,8 +54,12 @@
<p>These controls ship in every release today. No federal-specific build required to use them.</p>
<ul class="feature-list">
<li><strong>Hardened TLS posture.</strong> The TLS 1.2 cipher suite list, curve preferences, and HSTS header are pinned to the federal TLS guidance (NIST SP 800-52 Rev. 2). Only AEAD ciphers; only forward-secret key exchanges; only modern curves. Weak suites cannot be negotiated even if a client offers them.</li>
<li><strong>Pluggable policy engine.</strong> Access decisions can be delegated to an external <a href="https://www.openpolicyagent.org/" rel="noopener">Open Policy Agent</a> server with the customer's own audited Rego rules. The default in-process engine and the OPA-delegated engine speak the same wire format, so customers swap by setting one environment variable.</li>
<li><strong>Role-based access control (NIST AC-2 / AC-3).</strong> A <code class="inline">.zddc</code> file declares named roles whose members are email patterns; permissions reference role names rather than pasting the same wildcards across every directory. Role definitions cascade and child files shadow ancestors for that subtree, so federation between an organisation-wide role definition and a project-specific override is built in. Maps cleanly to AC-3(7) "role-based" enforcement and AC-2 account-management workflows.</li>
<li><strong>Verb-based least privilege (NIST AC-6).</strong> Every access decision evaluates one of five explicit verbs — <code class="inline">r</code> read, <code class="inline">w</code> overwrite, <code class="inline">c</code> create, <code class="inline">d</code> delete, <code class="inline">a</code> admin / edit ACL. A grant of <code class="inline">rc</code> means "this principal can read existing files and create new ones, but cannot modify or delete what's already there" — exactly the permission shape an immutable archive needs. Empty grants (<code class="inline">""</code>) are explicit denies that beat any other grant at the same level.</li>
<li><strong>Write-once-read-many archive folders (NIST AU-9, MP-5).</strong> Files placed under <code class="inline">archive/&lt;party&gt;/issued/</code> or <code class="inline">archive/&lt;party&gt;/received/</code> are protected by a server-enforced verb mask: ancestor grants are reduced to read-only when crossing the WORM boundary, regardless of what an upstream <code class="inline">.zddc</code> says. A separate <code class="inline">.zddc</code> placed at the WORM folder itself can grant <code class="inline">rc</code> to specific principals (the doc-controller dropping a fresh transmittal) — that survives the mask. Root admins bypass the mask only as the deliberate escape hatch for mis-filed documents, with bypasses visible in the audit log.</li>
<li><strong>Pluggable policy engine.</strong> Access decisions can be delegated to an external <a href="https://www.openpolicyagent.org/" rel="noopener">Open Policy Agent</a> server with the customer's own audited Rego rules. The server POSTs request user, path, action, and the full <code class="inline">.zddc</code> cascade chain to <code class="inline">/v1/data/zddc/access/allow</code>; the customer's OPA returns allow/deny. Default in-process engine and OPA-delegated engine speak the same wire format, so customers swap by setting one environment variable. Failures fail closed by default; <code class="inline">ZDDC_OPA_FAIL_OPEN=1</code> flips it for the rare case where availability outranks confidentiality.</li>
<li><strong>Strict-least-privilege policy variant available out of the box.</strong> A parity-tested federal-mode Rego that enforces NIST AC-6 (parent denies are absolute; no leaf-level override) ships embedded in the binary. <code>zddc-server --print-rego=federal</code> emits it for use with the customer's OPA.</li>
<li><strong>Cascade tracer for reviewers (NIST AC-3 reviewability).</strong> Admins can hit <code class="inline">/.profile/effective-policy?path=&lt;url&gt;</code> on a running server to see the resolved ACL chain at any path — every directory's grants, the role evaluation, and the final verb-set returned for the requesting user. A security reviewer can confirm "yes, this person has exactly these rights at this path" without reading the source. Helpful during accreditation and for incident response.</li>
<li><strong>Structured audit logging.</strong> Every request is logged with the authenticated email, method, path, status, response size, and duration. Logs are JSON-line, ready for fluentd / Vector / SIEM pipelines.</li>
<li><strong>Documented vulnerability-disclosure process.</strong> A <a href="https://codeberg.org/VARASYS/ZDDC/src/branch/main/SECURITY.md">SECURITY.md</a> covering supported versions, reporting channel, response timeline, embargo workflow, and CVE assignment.</li>
<li><strong>Cascading access control with explicit trust boundaries.</strong> The <code class="inline">.zddc</code> ACL model has documented invariants — root-only admin escalation gate, subtree-author authority limited to their subtree, default-deny when any <code class="inline">.zddc</code> exists. The <a href="https://codeberg.org/VARASYS/ZDDC/src/branch/main/zddc/README.md#access-control-the-zddc-cascade">access-control reference</a> includes a five-minute verify-it recipe a security reviewer can run on their own deployment.</li>
@ -109,12 +113,12 @@
<td style="padding: var(--spacing-sm); vertical-align: top;">Today the proxy and zddc-server trust each other via network isolation. For federal, the recommended path is a signed forwarding token (JWT) — proxy signs each request with its private key, zddc-server verifies with the published public key. mTLS available as an alternative when the customer already operates a private CA.</td>
</tr>
<tr style="border-bottom: 1px solid var(--color-border);">
<td style="padding: var(--spacing-sm); vertical-align: top;"><strong>Role-based access control</strong></td>
<td style="padding: var(--spacing-sm); vertical-align: top;">The current <code class="inline">.zddc</code> model grants access by email pattern. Federal AC-3(7) wants role-based grants populated from the customer's identity provider. The cascade syntax extends to express role allow/deny alongside email allow/deny; roles flow through the same OPA hook so customers running their own Rego pick them up automatically.</td>
<td style="padding: var(--spacing-sm); vertical-align: top;"><strong>Identity-provider role sync</strong></td>
<td style="padding: var(--spacing-sm); vertical-align: top;">Role-based grants are already in the <code class="inline">.zddc</code> model — see the &quot;Role-based access control&quot; row above. What an integrator adds for federal is a sync layer that populates a role's <code class="inline">members:</code> list from the customer's identity provider (Active Directory groups, Okta, EntraID) on a schedule, so role membership tracks the IdP rather than being maintained by hand. The cascade format already accepts role members; this is the wiring that keeps them in sync.</td>
</tr>
<tr style="border-bottom: 1px solid var(--color-border);">
<td style="padding: var(--spacing-sm); vertical-align: top;"><strong>Policy export for change control</strong></td>
<td style="padding: var(--spacing-sm); vertical-align: top;">A command that walks the served tree and emits every directory's resolved access policy in JSON / Markdown / CSV. The change-control workflow checks the export into a Git repo; every <code class="inline">.zddc</code> change produces a diff that reviewers approve before deploy. NIST CM-3.</td>
<td style="padding: var(--spacing-sm); vertical-align: top;">The cascade tracer (<code class="inline">/.profile/effective-policy</code>) already returns a resolved policy for any single path. What's missing is a batch export — a command that walks the entire served tree and emits every directory's resolved access policy in JSON / Markdown / CSV. The change-control workflow checks the export into a Git repo; every <code class="inline">.zddc</code> change produces a diff that reviewers approve before deploy. NIST CM-3.</td>
</tr>
<tr>
<td style="padding: var(--spacing-sm); vertical-align: top;"><strong>Code-signed tool fetches</strong></td>

View file

@ -917,42 +917,53 @@ date = 4DIGIT "-" 2DIGIT "-" 2DIGIT
<!-- Section 9: Project layout & transmittal workflow -->
<section id="transmittal-workflow">
<h2>9. Project layout &amp; transmittal workflow</h2>
<p>A project is a directory containing a single <code>.zddc</code> file. The server materialises the canonical folders below on the first write into them — drop a <code>.zddc</code> in an empty directory, point <code>zddc-server</code> at it, and the layout comes to life as content arrives. Folder names are matched case-insensitively, so a manually-created <code>Working/</code> is reused rather than shadowed by a new <code>working/</code>.</p>
<p>A ZDDC project mirrors the natural lifecycle of an engineering deliverable: <strong>drafted in private, lined up for issue, formally exchanged, kept as record</strong>. Each canonical folder maps to one of those stages, so file location alone tells you where a document is in its lifecycle. An operator only needs to create one file — a <code>.zddc</code> in an empty directory — and the rest of the layout populates as work happens.</p>
<div class="code-block">
project/
.zddc ← the only file an operator must create
working/ ← user-owned drafting workspace; mdedit
for markdown, browse for uploads.
Each authenticated user with access
sees a virtual <em>&lt;your-email&gt;/</em>
subfolder; first write materialises it.
staging/ ← outbound-transmittal preparation. A
transmittal-named mkdir here also
creates a same-named drafting folder
under working/ as a sibling space.
reviewing/ ← virtual cross-reference of in-progress
review responses; never on disk.
Notes live in working/&lt;rs-name&gt;/.
archive/ ← formal record of issued/received
transmittals, organised by party.
{party-name}/ ← one per counterparty AND one for
ourselves; self-folder is symmetric.
mdl/ ← Master Deliverables List for that
party. Visiting the folder opens the
table editor (<code>mdl.table.html</code>).
Default schema is built into the
server; per-party overrides via a
tables: { mdl: ./mdl.table.yaml }
entry in the party's .zddc.
incoming/ ← that party's drop point. Auto-own
.zddc grants the creator of each new
subfolder full control.
received/ ← permanent record of incoming we've
accepted (WORM — write-once read-many).
issued/ ← permanent record of what we sent to
that party (WORM).
.zddc ← the only file an operator must create
working/ ← Where staff draft. Each person has their own
subfolder here (named by email) and works on
documents in private until they're ready to
issue. Markdown opens in mdedit; arbitrary
files are dropped in via the browse tool.
staging/ ← The "about to issue" lane. A folder in here
declares a planned outbound transmittal: its
name carries the planned issue date and tracking
number, so anyone can see what's queued up and
when. Files in staging are the finalised set;
drafting still happens in working/.
reviewing/ ← One place to see everything we owe a response
on. Each row pairs an inbound submittal we've
accepted with our in-progress response draft —
saves staff from hunting across per-party
folders to find what's open.
archive/ ← The permanent record. Everything we've ever
formally exchanged with a counterparty lives
here, organised one folder per party.
{party-name}/ ← One per counterparty; one for ourselves.
We treat our own organisation as just another
party so internal deliverables get the same
tracking treatment as external ones.
mdl/ ← The party's Master Deliverables List —
what they're going to produce, planned and
in-flight. Opens as a grid editor; rows are
individual yaml files, one per deliverable.
incoming/ ← Where that party drops things for us. Acts
as a quality-check buffer before content
becomes part of the permanent record.
received/ ← What we've accepted from that party. Immutable;
the historical record of inbound documents.
issued/ ← What we sent to that party. Immutable; the
historical record of outbound documents.
</div>
<p style="margin-top: var(--spacing-md); font-size: 0.95em; color: var(--color-text-soft);"><em>Mechanics:</em> folders materialise on first write, names match case-insensitively, the staging↔working pairing is automatic, the immutable folders enforce write-once via an ACL mask, and a virtual <em>&lt;your-email&gt;/</em> entry under <code>working/</code> creates your personal subfolder on first save. None of that needs to be in your head when you're using the system — drop files where the lifecycle says they go and the layout takes care of itself.</p>
<p><strong>5-step transmittal workflow:</strong></p>
<table>