feat(deploy): add canonical Gitea recovery flows
This commit is contained in:
@@ -81,9 +81,347 @@ Supported components in this source:
|
||||
- `n8n-private-extension`
|
||||
- `module-foundry`
|
||||
- `device-plane`
|
||||
- `gitea`
|
||||
- `proxy-contur`
|
||||
- `dc-amd-proxy`
|
||||
|
||||
## Fresh Gitea installation
|
||||
|
||||
`gitea` is a one-time, fresh-install-only component. Its artifact contains
|
||||
only the reviewed Compose file and strict deployment descriptor; it contains
|
||||
no database, repository, user, token, key, hook, runtime data or secret. Build
|
||||
the deterministic artifact locally with:
|
||||
|
||||
```bash
|
||||
node infra/deploy-runner/build-gitea-fresh-install-artifact.mjs \
|
||||
gitea-fresh-install-YYYYMMDD-NNN
|
||||
```
|
||||
|
||||
The runtime is fixed to Gitea `1.27.1-rootless` by exact digest and
|
||||
`linux/amd64`. The runner never pulls it: the exact image must first be loaded
|
||||
through a separately reviewed trusted image-acquisition step. Compose is
|
||||
invoked with `--pull never --no-deps`, uses `network_mode: none`, publishes no
|
||||
ports, and serves only the host-mounted Unix socket
|
||||
`/volume1/docker/nodedc-gitea/socket/gitea.sock`. It exposes no SSH port, uses
|
||||
fresh SQLite, enables no LFS, and mounts two runner-generated secrets by URI.
|
||||
Registration and password Basic auth are disabled, while MFA is enforced;
|
||||
hooks, webhooks, migrations, packages, Actions, OAuth2/OpenID, federation and
|
||||
update checks are disabled. HTTP Git with PAT remains the intended trusted
|
||||
client path; PAT clone/push acceptance belongs to the later reviewed
|
||||
user/repository restoration transition, not this empty installation. LFS may
|
||||
only be enabled by that later transition after its secret and restart behavior
|
||||
has been accepted against the pinned runtime.
|
||||
|
||||
Plan and apply fail closed unless all live prerequisites are already true:
|
||||
|
||||
- `/volume1/docker/nodedc-gitea`, the `nodedc-gitea` project, and any stale
|
||||
`nodedc-gitea_internal` network are absent; TCP/4022 is closed and Docker
|
||||
publishes neither TCP/3000 nor TCP/4022;
|
||||
- Docker Server is exactly 24.0.2, Compose is at least 2.20.1, and the exact
|
||||
pinned image ID and RepoDigest are local for linux/amd64 with image user
|
||||
`1000:1000`;
|
||||
- the separate root-owned Nginx bridge
|
||||
`/usr/local/etc/nginx/conf.d/http.nodedc-gitea-uds.conf` matches its exact
|
||||
reviewed bytes, owner/mode and SHA-256; Nginx 1.23.1 configuration validates,
|
||||
and only its root/uid-1023 processes own `127.0.0.1:3000`. The bridge proxies
|
||||
to the Unix socket, preserves Host/X-Real-IP/XFF/XFP, rate-limits login and
|
||||
bounds concurrent requests without limiting Git request bodies;
|
||||
- DSM reverse-proxy UUID `5bc46027-0307-4261-af7e-4f94a3c508c9` persistently
|
||||
and in generated nginx config routes `git.dcserve.ru:443` to
|
||||
`127.0.0.1:3000`; the generated vhost must also retain its exact host guard
|
||||
and canonical Host/X-Real-IP/XFF/XFP forwarding directives, so the UDS
|
||||
bridge never trusts a client-supplied `X-Real-IP`;
|
||||
- the emergency broad INPUT DROP for TCP/3000 has been removed, while the
|
||||
legacy `172.22.0.222:3000` OUTPUT DROP and TCP/4022 INPUT DROP remain;
|
||||
- any legacy container named `gitea` is stopped with restart policy `no`.
|
||||
|
||||
Changing DSM reverse proxy/firewall rules, acquiring evidence from the old
|
||||
root, restoring trusted users/repositories, and loading the pinned image are
|
||||
separate reviewed transitions. This component never reads or mounts
|
||||
`/volume1/docker/gitea` and never starts the legacy container. Failed apply
|
||||
stops/removes only the `nodedc-gitea` candidate, proves that it is absent, and
|
||||
quarantines the complete newly created root (including data/config/socket/secrets) for
|
||||
evidence, and restores the absent source/root state without deleting runtime
|
||||
evidence. If candidate absence cannot be proven, rollback preserves the root in
|
||||
place and records reconciliation-required instead of disconnecting live bind
|
||||
mounts.
|
||||
|
||||
Run the policy suite before publishing:
|
||||
|
||||
```bash
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
python3 infra/deploy-runner/test_gitea_fresh_install.py -v
|
||||
```
|
||||
|
||||
## Gitea incident salvage (clean-state recovery)
|
||||
|
||||
The additive `gitea-incident-salvage` slice is the reviewed recovery design for
|
||||
incident `gitea-20260814`. It does not upgrade or mount the compromised state.
|
||||
The candidate starts with a new SQLite database, new config, new runtime
|
||||
secrets, new email addresses and new password hashes. The exact v2 decision
|
||||
bundle is embedded as inert, digest-bound data:
|
||||
|
||||
- 2 new active identities (`dctouch`, admin; `SILVER`, non-admin), each with a
|
||||
new root-contained one-time password and mandatory password change;
|
||||
- 8 new locked identities with random discarded passwords, inactive,
|
||||
restricted and prohibited from login;
|
||||
- 45 exact repositories: 32 private `dctouch` repositories and 13 public
|
||||
`SILVER` repositories. The other 962 legacy users and 2013 legacy repository
|
||||
rows are not imported into the clean database.
|
||||
|
||||
The successor v3 descriptor embeds the owner-confirmed, canonical
|
||||
`confirmed-disposition-v1.json` and the additive
|
||||
`confirmed-closure-disposition-v1.json`. Both are bound to the exact snapshot,
|
||||
database, identity decision, reference manifest, unsupported-state report,
|
||||
schema catalog and semantic-topics hashes. The original disposition's 105
|
||||
reference decisions are explicit rows, not namespace wildcards: 85 normal
|
||||
heads, 4 tags and 4 wiki heads are eligible for later live reconstruction; the
|
||||
exact 5 pull refs and 7 remote refs are sealed archive-only evidence and can
|
||||
never be auto-promoted. The closure disposition authorizes only bounded
|
||||
plan-time inventory and records the target policy for access, collaboration,
|
||||
issue/PR metadata, attachments, releases, labels, projects, units, Packages
|
||||
and Actions. Observed hashes do not become activation authority, and the new
|
||||
closure-report digest is deliberately unpinned until separate review.
|
||||
|
||||
Build the deterministic review artifact locally with:
|
||||
|
||||
```bash
|
||||
node infra/deploy-runner/build-gitea-incident-salvage-artifact.mjs \
|
||||
gitea-incident-salvage-subrelation-closure-20260814-006
|
||||
```
|
||||
|
||||
The original v1 descriptor and `...salvage-20260814-001.tgz` remain immutable.
|
||||
The exact `...salvage-disposition-20260814-002.tgz` also remains immutable and
|
||||
must not be retried: its plan failed safely because it assumed every nonempty
|
||||
`repository.topics` TEXT value was a JSON array. A bounded immutable-database
|
||||
classification proved all 45 kept rows are instead exact four-byte canonical
|
||||
JSON `null`, Gitea's semantic empty representation. The `...topics-...-003`
|
||||
artifact is the forward fix with a new patch id and digest.
|
||||
That exact `...topics-...-003` artifact is now also an immutable predecessor.
|
||||
The local pre-audit `...closure-...-004` draft must not be staged: review found
|
||||
orphan-join and nullable-Actions gaps. The local
|
||||
`...closure-reviewed-...-005` artifact is also stale and must not be staged:
|
||||
its primary closure was fail-closed, but it retained a broad polymorphic-
|
||||
subrelation blocker. The additive `...subrelation-closure-...-006` successor
|
||||
closes the concrete comment/history/merger relations in the pinned snapshot
|
||||
schema and is the only artifact produced by the current builder.
|
||||
The successor builder refuses to overwrite an existing artifact basename and
|
||||
publishes from an exclusively created same-directory temporary file using an
|
||||
atomic no-clobber hard link. A failed build removes only its private temporary
|
||||
file. The disposition is sorted, compact ASCII JSON with one terminal LF.
|
||||
|
||||
The runtime pin is
|
||||
`docker.gitea.com/gitea:1.27.2-rootless@sha256:7de5f49a…3abd2c`, with exact
|
||||
linux/amd64 OCI image ID `sha256:272085a8…bc5c4c6`. It runs as `1000:1000`,
|
||||
uses the direct Gitea binary, `network_mode: none`, no Docker ports, no SSH and
|
||||
no LFS. Candidate restart policy is `no`. The existing reviewed Nginx bridge
|
||||
cannot reach the candidate while the socket parent is mode `0700`; publication
|
||||
is an explicit post-acceptance change to `0750`, followed by public TLS and
|
||||
smart-HTTP checks. Only after those checks may restart policy become
|
||||
`unless-stopped`. The transition never changes or enforces two-factor
|
||||
authentication.
|
||||
|
||||
The snapshot boundary is exact: UUID
|
||||
`f5a3fe3a-93ea-bb4d-847f-6221a6bcbc9f`, immutable SQLite SHA-256
|
||||
`8db9e74a…1a13052`, and the 972/2058 decisions must recompute from the database
|
||||
row by row. The runner enumerates only each v2 `repo_relative_path` and
|
||||
`wiki_relative_path`. It rejects symlinks, special files, hard links, NOCOW,
|
||||
alternates, shallow/replace/grafts/worktrees, promisor state, unknown object
|
||||
material and incomplete pack/index pairs. One preloaded mount inventory also
|
||||
guards the `objects` and `refs` roots plus every traversed directory and file:
|
||||
all must remain on the trusted snapshot device, outside nested mountpoints and,
|
||||
for directories, outside nested Btrfs subvolumes. Traversal errors fail closed.
|
||||
Forbidden-path probes (`objects/info/alternates`, grafts, replace refs and the
|
||||
other listed sentinels) lstat each path component without following links and
|
||||
validate every ancestor before probing its child; a symlink or mount boundary
|
||||
therefore stops the plan before any lookup can escape the snapshot tree.
|
||||
|
||||
Only three exact derived-file classes observed in the bounded 45-repository
|
||||
inventory are excluded: dumb-HTTP `objects/info/packs`,
|
||||
`objects/pack/pack-<oid>.bitmap` with its exact complete pack/index pair, and
|
||||
monolithic `objects/info/commit-graph`. Each must be a bounded regular,
|
||||
single-link, non-NOCOW file. The reference manifest records its exact path,
|
||||
class and byte size, but the runner neither reads nor copies the excluded
|
||||
bytes. Unobserved `.rev`, MIDX and split commit-graph layouts stay fail-closed;
|
||||
`.promisor`, `.mtimes`, `.keep`, unknown and temporary lookalikes are not
|
||||
classified as disposable accelerators.
|
||||
|
||||
The same inventory found receive-pack quarantine directories matching only an
|
||||
exact direct child `objects/tmp_objdir-incoming-[A-Za-z0-9]{6}`. A match must
|
||||
retain the reviewed uid/gid `1000:1000`, mode `0755`, single-link and non-NOCOW
|
||||
directory metadata. Its exact path and lstat fields are recorded, then the
|
||||
subtree is pruned without reading, hashing, copying or reflinking contents; the
|
||||
sealed read-only incident snapshot retains the forensic copy. Any name,
|
||||
metadata, mount or subvolume drift remains a hard stop. A reviewed reference/OID
|
||||
manifest is required; raw HEAD/refs files are never copied. The intended data
|
||||
path is same-filesystem per-file `--reflink=always`, with no byte-copy fallback
|
||||
and a bounded exclusive-allocation gate.
|
||||
|
||||
This slice is intentionally **NO-GO for apply** at the current review point.
|
||||
`plan` inventories and prints the exact ref-manifest digest, byte length and
|
||||
canonical compact JSON. The JSON is the reviewable, hash-bound reference/OID
|
||||
artifact: it contains approved repository identities, ref names/OIDs, object
|
||||
totals and stat-bound derived/quarantine exclusions, but no Git object bytes,
|
||||
hooks or config. The same plan prints the canonical unsupported-state report
|
||||
and a separate incident-closure report, each with its SHA-256, byte length and
|
||||
compact ASCII JSON. The closure report is evidence only: its disposition keeps
|
||||
`expectedSha256` and `expectedBytes` null and retains an explicit review-pin
|
||||
blocker.
|
||||
|
||||
The reviewed plan evidence bound by the disposition is reference manifest
|
||||
`9cddaf0e…a311` (36,010 bytes), unsupported-state report
|
||||
`4b2cecf8…106ac` (119,468 bytes), and schema catalog
|
||||
`b5e3b677…b6db`. The report has exact 45 repository rows with empty
|
||||
`schema_missing`, `schema_mismatch`, and `anomalies` arrays. These facts close
|
||||
only the owner's material-state decision; fsck/reachability, physical blob
|
||||
inventory, sanitized archive creation and candidate acceptance remain separate
|
||||
verifiers.
|
||||
|
||||
The unsupported-state report has one deterministic row for every one of the
|
||||
45 kept repository IDs, including explicit zeroes. It separates direct
|
||||
repository-relation counts, directional base/head PR counts, repo-unit type
|
||||
counts, text-metadata presence booleans and denormalized numeric hints; it does
|
||||
not add overlapping relations into a misleading grand total. LFS rows are
|
||||
reported per repository with validated logical byte sums and distinct-OID
|
||||
aggregates, including size conflicts and sharing with non-kept repositories.
|
||||
Every association sharing a kept OID must also have an integer repository ID
|
||||
that resolves to the exact snapshot `repository` table; invalid and orphan
|
||||
owners make shared/unique byte aggregates unavailable and add a data-anomaly
|
||||
blocker.
|
||||
Attachments are reported per repository with validated logical byte sums and
|
||||
explicit, overlapping issue/comment/release/unlinked link splits. Neither
|
||||
category claims that a physical blob exists; no title, body, note, comment,
|
||||
config, URL, token, key, secret, hash payload or blob content is selected or
|
||||
exported.
|
||||
|
||||
The report also embeds the exact `PRAGMA table_xinfo` catalog and explicit
|
||||
label-to-table-to-repository-column map for a code-owned allowlist of relevant
|
||||
tables. `sqlite_master` must prove an ordinary, non-virtual table before any
|
||||
row query; SQLite 3.37+ adds `PRAGMA table_list` ordinary-main-table proof. On
|
||||
an older compatible SQLite, the weaker object-kind capability is recorded and
|
||||
adds a coverage blocker. `query_only=ON` and `trusted_schema=OFF` are read back
|
||||
before the snapshot connection is accepted. Only column identifiers and
|
||||
structural metadata are exported, never DDL, defaults or row values. The v3
|
||||
closure inventory follows code-owned, schema-attested primary joins for the
|
||||
issue/PR dependency tables (comments, assignees, content histories, labels, issue
|
||||
users/watches, reactions, reviews/state, dependencies, notifications,
|
||||
stopwatches, tracked time and auto-merge), plus projects/boards, releases,
|
||||
attachments, Packages and Actions. It emits only IDs required to prove joins,
|
||||
actor classes, counts, declared sizes and SQLite byte lengths; it never selects
|
||||
or prints text bodies, names, URLs, package metadata, workflow payloads,
|
||||
credentials, tokens, secrets or logs. Package and Actions target features stay
|
||||
disabled and no legacy row is importable. The successor additionally closes
|
||||
every concrete FK-like comment field in the pinned schema (labels, projects,
|
||||
milestones, tracked time, assignees, dependency, review and cross-reference),
|
||||
content-history-to-comment, review-team and pull-merger provenance. Target
|
||||
repository and local actor IDs are classified through the exact 2058/972
|
||||
decisions. Migrated external-author IDs are never treated as local users and
|
||||
are represented only by presence counts and author-name byte lengths. A
|
||||
nonzero comment-assignee or review-reviewer team must resolve to an exact team
|
||||
and organization; because no organization/team allowlist exists, its row,
|
||||
team and organization IDs enter a sealed HOLD inventory and add the narrowly
|
||||
scoped `issue-pr-team-mapping-hold` blocker. Orphans, conflicting user/team
|
||||
identities, invalid cross-reference pairs and schema/type drift fail closed.
|
||||
The report is still not a physical-file or sanitized-archive verifier, so
|
||||
those separate blockers remain explicit.
|
||||
The schema-catalog digest is recorded as observed evidence but deliberately
|
||||
remains unpinned as reviewed activation authority. Schema absence/type drift,
|
||||
invalid numeric/OID state or aggregate overflow is a hard error. `apply` fails
|
||||
before creating `/volume1/docker/nodedc-gitea` until all of the following are
|
||||
registered:
|
||||
|
||||
- root-owned Btrfs proof that the named snapshot UUID is read-only;
|
||||
- exact stopped/restart-`no` legacy container image ID, image ref and sole
|
||||
`/volume1/docker/gitea:/data` bind inspect;
|
||||
- a separately verified exact reference/OID manifest digest, fsck,
|
||||
reachability and selected-object reconstruction;
|
||||
- immutable sanitized archives for issues, PRs, comments, releases, labels,
|
||||
projects and repository descriptions, with no legacy row import;
|
||||
- physical attachment inventory and reachable LFS pointer/OID/size/SHA proof;
|
||||
- kept-user mapping before any collaboration recreation; and
|
||||
- package/Actions schema plus physical zero closure and target unit-policy
|
||||
acceptance.
|
||||
|
||||
All 25 direct repository relations have explicit dispositions, including the
|
||||
15 exact zero-and-drop categories. Pull base/head counts remain directional
|
||||
views of the same five PRs. All repository numeric hints are dropped and
|
||||
recomputed. Repo-unit types 1–10 are total: clean types 1–5 and 8 are recreated
|
||||
once per repository; external wiki/tracker are forbidden; Packages (9) and
|
||||
Actions (10) are absent in the target and remain globally disabled. Legacy
|
||||
unit config is never imported. The topics verifier accepts exact JSON `null`
|
||||
as semantic empty state, or a canonical JSON array of sorted unique lowercase
|
||||
names matching `^[a-z0-9][-.a-z0-9]*$` with at most 35 UTF-8 bytes. SQL NULL,
|
||||
whitespace/case variants, quoted `"null"`, objects, numbers, booleans and
|
||||
noncanonical arrays fail closed. The current snapshot must prove exactly 45
|
||||
serialized JSON nulls, zero serialized arrays, zero material topics and zero
|
||||
`repo_topic` rows; target cache is rebuilt from relations.
|
||||
|
||||
The activation hook remains frozen until these blockers are closed and the
|
||||
full clean-database recreation, reflink reconstruction, doctor/fsck,
|
||||
credential-table zeroing, rollback and public acceptance path has its own
|
||||
runtime smoke. The current reviewed source identities are:
|
||||
|
||||
- runner `nodedc-deploy` SHA-256
|
||||
`c766985aa02fe911fa5a873717276f3503d9cecded448e41dc6945030dcbce9e`;
|
||||
- deterministic builder SHA-256
|
||||
`b92076583aeab854015a05deb9d942c8dfcf3ca475fd3d52115d402d6f61acca`;
|
||||
- salvage policy test SHA-256
|
||||
`99b96b6179286a78e7d53ee6a923fc092e3471a8cece382a472449d455ad212c`;
|
||||
- fresh-install policy test SHA-256
|
||||
`20653c640142c25e7d2ef712048acc7889c56e1d519e2d03b5cad4127e48947c`;
|
||||
- confirmed disposition SHA-256
|
||||
`0a066724bcf6e4933133db6cab6cc273393e3c262dd00dda0bbf9ceebd84f78c`;
|
||||
- confirmed closure disposition SHA-256
|
||||
`7ed66d9848268431a703fe24b22c41afbaa7c5ff48949604d6fc448d93e0d243`;
|
||||
- successor v3 descriptor SHA-256
|
||||
`9b98eb1a1640fd5569cf051a621837379b167eff4527313a43a0a851e7cc181a`;
|
||||
- deterministic local review artifact
|
||||
`nodedc-gitea-gitea-incident-salvage-subrelation-closure-20260814-006.tgz`
|
||||
SHA-256
|
||||
`d3e598cf892b1371912dcd9cef64caa991dd23613e48fe979d0f95d493c23d53`.
|
||||
|
||||
Only after that exact runner is separately reviewed, promoted to the
|
||||
root-owned runner path and its installed SHA-256 is re-attested may the exact
|
||||
artifact be staged into the canonical inbox for a canonical `plan` command.
|
||||
Staging is evidence collection only; it grants no authority to run `apply`.
|
||||
The legacy identity is now pinned to the observed ref `gitea/gitea:latest`
|
||||
and immutable image ID
|
||||
`sha256:bf95d9a45ce4fe38b027d051cdc4a4bc531513489fa6244af4074efbb1c376d6`.
|
||||
The mutable tag is provenance only. Every plan re-inspects exact name
|
||||
`/gitea`, stopped state, restart `no`, and the sole RW bind
|
||||
`/volume1/docker/gitea:/data`; drift is a hard stop.
|
||||
|
||||
The successor plan must report these exact remaining blockers:
|
||||
|
||||
- `attachment-physical-verifier-pending`;
|
||||
- `candidate-root-activation-hard-frozen`;
|
||||
- `closure-report-review-pin-pending`;
|
||||
- `collaboration-kept-user-mapping-verifier-pending`;
|
||||
- `forensic-ref-archive-verifier-pending`;
|
||||
- `issue-pr-metadata-sanitized-archive-verifier-pending`;
|
||||
- `lfs-reachable-pointer-physical-verifier-pending`;
|
||||
- `package-action-physical-closure-verifier-pending`;
|
||||
- `reference-manifest-fsck-reachability-verifier-pending`;
|
||||
- `repository-object-reconstruction-verifier-pending`;
|
||||
- `target-unit-policy-acceptance-pending`;
|
||||
- `unsupported-schema-catalog-verifier-pending`.
|
||||
|
||||
Any other blocker or identity drift is also a stop. `apply` remains prohibited
|
||||
and hard-frozen before root creation until a subsequent reviewed runner
|
||||
revision closes every blocker and implements and smoke-tests the complete
|
||||
activation/rollback path. The successor files must be independently reviewed
|
||||
before any runner promotion or inbox staging.
|
||||
|
||||
Run the bounded policy suite with:
|
||||
|
||||
```bash
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
python3 infra/deploy-runner/test_gitea_incident_salvage.py -v
|
||||
```
|
||||
|
||||
The frozen local cutoff passes 54 incident-salvage tests and 26 fresh-install
|
||||
tests, plus Python compilation and `git diff --check`.
|
||||
|
||||
`n8n-private-extension` is a staging-only trust boundary for reviewed offline
|
||||
n8n private-node releases. Its artifact may contain exactly one digest-bound
|
||||
`n8n-nodes-ndc` release with `package.tgz`, `release.json` and
|
||||
|
||||
Reference in New Issue
Block a user