Compare commits

..

2 commits

Author SHA1 Message Date
6aae181d19 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>
2026-05-07 09:42:45 -05:00
f81fb4e769 docs: canonical folder layout, role-based ACL, WORM, lazy creation
Update reference.html § 9 (transmittal workflow): replace the legacy
per-party tree (project/{party-name}/{incoming,received,issued}) with
the current canonical layout — project root has working/, staging/,
reviewing/, archive/, and per-party folders sit under
archive/<party>/{mdl,incoming,received,issued}/. Note lazy creation,
case-fold matching, the per-user virtual <viewer-email>/ entry, mdl
opening the table editor, and the staging↔working drafting mirror.
Add a "Drafting a response transmittal" subsection describing how
inbound submittals (-SUB- @ IFR/IFA) flow through staging→working
into archive/<party>/issued/ as RS* responses.

Update index.html "Access control via .zddc files" bullet to describe
what the server actually does today: cascade direction, the five
verbs (r/w/c/d/a), explicit deny via empty grant, and the
X-Auth-Request-Email convention. Add new bullets for roles (with a
short YAML example), WORM archive folders + drop-in producer pattern,
lazy folder creation + case-fold matching, the cascade tracer
admin endpoint, and an expanded OPA paragraph (input shape, cache
TTL, fail-open flag, --print-rego=federal). Update the install card's
tool-folder list to use lowercase canonical names, mention browse,
and add mdl.table.html as the per-party MDL view.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 09:30:31 -05:00
3 changed files with 83 additions and 18 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

@ -175,9 +175,21 @@
<p style="margin-top: var(--spacing-md);"><strong><code class="inline">zddc-server</code></strong> is a small Go binary purpose-built to serve ZDDC archives. <em>Any</em> web server gives you online mode; <code class="inline">zddc-server</code> adds things a generic web server can't:</p>
<ul class="feature-list">
<li><strong>Access control via <code class="inline">.zddc</code> files.</strong> Behind a reverse proxy that authenticates users and sets an <code class="inline">X-Auth-Request-Email</code> request header, <code class="inline">zddc-server</code> consults YAML <code class="inline">.zddc</code> files in directories — cascading bottom-up; deeper rules override. Common shapes (paired open/closed projects + third-party-restricted vendor folders) are documented with worked examples in the <a href="https://codeberg.org/VARASYS/ZDDC/src/branch/main/zddc/README.md#access-control-the-zddc-cascade">access-control reference</a>. No database, no admin UI.</li>
<li><strong>OPA-compatible policy decider.</strong> Federal and other regulated customers can swap the built-in evaluator for an external <a href="https://www.openpolicyagent.org/" rel="noopener">Open Policy Agent</a> server with their own audited Rego policies — set <code class="inline">ZDDC_OPA_URL</code> and the same <code class="inline">.zddc</code> files become inputs to your engine instead of ours. Wire format is OPA-canonical (<code class="inline">POST /v1/data/zddc/access/allow</code>). Default mode adds zero new dependencies; external mode is a configuration flip.</li>
<li><strong>Access control via <code class="inline">.zddc</code> files.</strong> Behind a reverse proxy that authenticates users and sets an <code class="inline">X-Auth-Request-Email</code> request header, <code class="inline">zddc-server</code> consults YAML <code class="inline">.zddc</code> files at every directory along the path. The cascade walks root→leaf; the closest match wins. Five 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) gate every operation. An empty grant (e.g. <code class="inline">"*@vendor.com": ""</code>) is an explicit deny. Common shapes (paired open/closed projects, third-party-restricted vendor folders) are documented with worked examples in the <a href="https://codeberg.org/VARASYS/ZDDC/src/branch/main/zddc/README.md#access-control-the-zddc-cascade">access-control reference</a>. No database, no admin UI.</li>
<li><strong>Roles for human-readable grants.</strong> A <code class="inline">.zddc</code> may declare named roles whose members are email patterns; permissions then reference the role name instead of pasting the same wildcard everywhere:
<pre style="margin: 0.4rem 0;"><code>roles:
qc-reviewers:
members: ["*@quality.org", "alice@example.com"]
acl:
permissions:
qc-reviewers: rwd
"*@example.com": r</code></pre>
Role definitions cascade like everything else; a child <code class="inline">.zddc</code> redefining the same role name shadows the ancestor for that subtree.</li>
<li><strong>WORM archive folders.</strong> Anything under <code class="inline">archive/&lt;party&gt;/issued/</code> or <code class="inline">archive/&lt;party&gt;/received/</code> enforces write-once via a verb mask: ancestor grants are reduced to <code class="inline">r</code> only, while a <code class="inline">.zddc</code> placed at the WORM folder itself can still grant <code class="inline">rc</code> (create-but-not-overwrite) to specific principals — that's how a doc controller drops a fresh transmittal into the immutable record. Root admins (the <code class="inline">admins:</code> list in the root <code class="inline">.zddc</code>) bypass the mask as the deliberate escape hatch for mis-filed documents.</li>
<li><strong>Lazy folder creation, case-fold matching.</strong> Drop a <code class="inline">.zddc</code> file into an empty directory and the canonical project layout (<code class="inline">working/</code>, <code class="inline">staging/</code>, <code class="inline">archive/&lt;party&gt;/{mdl,incoming,received,issued}/</code>) materialises on the first write into each path — never on bare reads. Folder names are matched case-insensitively, so an existing <code class="inline">Working/</code> is reused rather than shadowed by a new <code class="inline">working/</code> sibling. Each authenticated viewer sees a virtual <code class="inline">working/&lt;your-email&gt;/</code> entry; first write makes it real.</li>
<li><strong>OPA-compatible policy decider.</strong> Federal and other regulated customers can swap the built-in evaluator for an external <a href="https://www.openpolicyagent.org/" rel="noopener">Open Policy Agent</a> server with their own audited Rego policies — set <code class="inline">ZDDC_OPA_URL</code> and the server POSTs the request's user, path, action, and the full <code class="inline">.zddc</code> cascade chain to <code class="inline">/v1/data/zddc/access/allow</code>. Decisions are cached per (user, path, action) with a configurable TTL (<code class="inline">ZDDC_OPA_CACHE_TTL</code>); failures fail closed by default (<code class="inline">ZDDC_OPA_FAIL_OPEN=1</code> flips it). The bundled NIST AC-6 strict-cascade preset is dumpable via <code class="inline">--print-rego=federal</code>. Default mode adds zero new dependencies; external mode is a configuration flip.</li>
<li><strong>Designed for regulated environments.</strong> Hardened TLS (NIST SP 800-52 Rev. 2 cipher allowlist + HSTS), pluggable policy engine, federal-mode strict-least-privilege Rego shipping out of the box, structured audit logging, documented vulnerability-disclosure process. Specific federal-track work (FIPS-validated build, signed-token proxy↔server channel, code-signed tool fetches) is on a clear roadmap — see the <a href="federal.html">federal compliance page</a> for the supported deployment shape and what an integrator adds during ATO.</li>
<li><strong>Cascade tracer for operators.</strong> Admins can hit <code class="inline">/.profile/effective-policy?path=&lt;url&gt;</code> to see the resolved ACL chain at any path — every level's grants, the role evaluation, the final verb-set. Useful when a permission isn't behaving the way the operator expected.</li>
<li><strong>Virtual <code class="inline">.archive</code> URL space.</strong> <code class="inline">GET /Project/.archive/123-XYZ.html</code> resolves to the canonical revision file at request time. Computed from filenames; no cache, no separate index file.</li>
<li><strong>Per-request access logging</strong> keyed to the authenticated user; conservative HTTP timeouts; optional file-tee for offline audit (production deployments typically leave logs on stdout for the orchestrator's pipeline to handle).</li>
<li><strong>TLS, ETags, conditional GET, CORS, autoindex.</strong> The mundane glue.</li>
@ -197,12 +209,14 @@
<h3>Server: just run zddc-server</h3>
<p class="when">The binary has the current-stable build of all five tools baked in at compile time. They appear automatically at the right paths under <code class="inline">ZDDC_ROOT</code>:</p>
<ul class="install-points">
<li><strong>archive.html</strong> at every level (root, project, archive, vendor)</li>
<li><strong>classifier.html</strong> in any <code class="inline">Incoming</code>, <code class="inline">Working</code>, or <code class="inline">Staging</code> directory and its subtree</li>
<li><strong>mdedit.html</strong> in any <code class="inline">Working</code> directory and its subtree</li>
<li><strong>transmittal.html</strong> in any <code class="inline">Staging</code> directory and its subtree</li>
<li><strong>archive.html</strong> and <strong>browse.html</strong> at every level (root, project, archive, party)</li>
<li><strong>mdedit.html</strong> in any <code class="inline">working/</code> directory and its subtree</li>
<li><strong>transmittal.html</strong> in any <code class="inline">staging/</code> directory and its subtree</li>
<li><strong>classifier.html</strong> in any <code class="inline">working/</code>, <code class="inline">staging/</code>, or <code class="inline">archive/&lt;party&gt;/incoming/</code> subtree</li>
<li><strong>mdl.table.html</strong> at every <code class="inline">archive/&lt;party&gt;/</code> — the per-party Master Deliverables List, served from a built-in default schema unless the party's <code class="inline">.zddc</code> declares a custom one</li>
<li><strong>index.html</strong> (the project picker) at the deployment root</li>
</ul>
<p class="when" style="margin-top: 0.6rem;">Folder names are case-insensitive — <code class="inline">Working/</code>, <code class="inline">working/</code>, and <code class="inline">WORKING/</code> all match the <code class="inline">working/</code> rule.</p>
<pre><code>ZDDC_ROOT=/srv/zddc ./zddc-server</code></pre>
<p class="when" style="margin-top: 0.6rem;"><strong>To override a tool</strong> at any path: drop a real <code class="inline">.html</code> file there — that file wins over the baked-in version. <strong>To pin a different version</strong>, write an <code class="inline">apps:</code> entry in any <code class="inline">.zddc</code> file along the path:</p>
<pre><code># &lt;project&gt;/.zddc

View file

@ -914,19 +914,57 @@ date = 4DIGIT "-" 2DIGIT "-" 2DIGIT
<p>Search the deliverable's own tracking number across all transmittal folders in <code>issued/</code> and <code>received/</code>. Every folder containing that number is part of its history: which package first carried it, what the response was, which resubmittal carried the revision, and what the final outcome was.</p>
</section>
<!-- Section 9: Transmittal workflow -->
<!-- Section 9: Project layout & transmittal workflow -->
<section id="transmittal-workflow">
<h2>9. Transmittal workflow</h2>
<p>Each third party (client, contractor, vendor, etc.) has a separate subfolder. All communication with that party lives in one place.</p>
<h2>9. Project layout &amp; transmittal workflow</h2>
<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/
{party-name}/
incoming/ ← transmittals received from party, awaiting acceptance
received/ ← permanent record of accepted transmittals from party
issued/ ← your copies of transmittals sent to party
.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><strong>5-step workflow:</strong></p>
<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>
<thead>
@ -974,6 +1012,15 @@ project/
<div class="highlight-box" style="margin-top: var(--spacing-lg);">
<p><strong>What SHA-256 gives you:</strong> Mathematical fingerprint of file contents. Single byte change → hash changes. When acknowledgment records hashes, you can verify years later that the file is identical to what was transmitted.</p>
</div>
<h3>Drafting a response transmittal</h3>
<p>Submittals from counterparties (tracking numbers containing <code>-SUB-</code> at status <code>IFR</code> or <code>IFA</code>) require a response transmittal whose status starts with <code>RS</code> (RSA, RSB, RSC, …). The drafting flow uses the same staging↔working pairing as a fresh outbound:</p>
<ol>
<li>Create <code>staging/&lt;YYYY-MM-DD&gt;_&lt;tracking&gt; (RSA) - &lt;title&gt;/</code>. The date is the planned issue (response-due) date.</li>
<li>The server mirrors the folder name into <code>working/</code>; reviewer notes and the response payload are drafted there.</li>
<li>The <code>reviewing/</code> virtual surface lists each in-progress response paired with the source submittal in <code>archive/&lt;party&gt;/received/</code>.</li>
<li>When the response is ready, files move from <code>working/</code> into <code>staging/</code> for sign-off, then through the standard 5-step transmittal flow into <code>archive/&lt;party&gt;/issued/</code>. Both the staging and working folders are deleted at issue time.</li>
</ol>
</section>
<!-- Section 10: Tools -->