feat: publish Tend v0.2 preview source
Publish the reviewed allowlisted snapshot whose exact binary completed maintenance deployment, rollback, and reactivation exercises for Gamertan and Sandwich Hime. Private-Source-Commit: 4d7094c8b7c61991bfb67b11fc1558724c874eb2 Private-Source-Tree: 54a2f74804f7acddf3755d7d4da5b97f5fc28381 AI-Assistance: OpenAI Codex assisted implementation, testing, security review, and release verification. Signed-off-by: Cole Speelman <crspeelman@gmail.com>
This commit is contained in:
+24
-9
@@ -17,12 +17,26 @@ requires byte-identical output. The resulting archive contains only:
|
||||
|
||||
The external archive digest is the release identity used by deployment.
|
||||
|
||||
## Transport boundary
|
||||
|
||||
`tend push` sends one bounded protocol frame to a dedicated account through a
|
||||
pinned OpenSSH host key. It uses an argument vector, disables config files,
|
||||
forwarding, PTYs, local commands, and proxy commands, and requests exactly
|
||||
`tend-receive-v1`. The account's forced command invokes only the root-owned
|
||||
receiver. The receiver's root-owned policy maps an allowlisted service name to
|
||||
one exact configuration path and size ceiling.
|
||||
|
||||
The target receives no source, repository credential, Go cache, dependency, or
|
||||
arbitrary command. The artifact digest must equal both the produced digest and
|
||||
the separately supplied approved digest before it is staged.
|
||||
|
||||
## Target-host boundary
|
||||
|
||||
`tend deploy` runs on the Linux target. Transfer and SSH authentication stay
|
||||
outside Tend v0.1; application-local transfer commands move the artifact and
|
||||
the identical Tend binary to a root-controlled staging directory. Tend then
|
||||
requires the expected digest to be repeated as a separately approved digest.
|
||||
Schema-2 configurations live below `/etc/tend/services/`. Secrets live only in
|
||||
separate `/etc/tend/environment/*.env` files that are root-owned, non-symlink,
|
||||
and mode `0600`. Configurations contain the path, never the values. Candidate
|
||||
and installed units read the same file; Tend overrides only the candidate's
|
||||
loopback listen address.
|
||||
|
||||
Dry-run validation extracts into a temporary directory and does not touch the
|
||||
release tree. Activation acquires the shared lock, installs a content-addressed
|
||||
@@ -39,8 +53,9 @@ Singleton mode starts the new release in a hardened transient systemd unit on a
|
||||
separate loopback address, probes it, stops the candidate, changes the current
|
||||
release pointer, and restarts the installed singleton unit.
|
||||
|
||||
Any failure before state persistence restores the previously observed Caddy
|
||||
bytes and/or release pointers. Rollback is a separate explicit command over the
|
||||
recorded state. It rechecks health and readiness, but deliberately does not
|
||||
apply the current release's content markers to an older release whose routes
|
||||
may differ. Pruning preserves both active and previous releases.
|
||||
After the local post-activation probes, Tend also checks configured HTTPS public
|
||||
origins. Any failure before state persistence restores the previously observed
|
||||
Caddy bytes and/or release pointers. Rollback is a separate explicit command
|
||||
over the recorded state. It rechecks local health/readiness and public reachability,
|
||||
but deliberately does not apply a future release's content marker to an older
|
||||
release whose routes may differ. Pruning preserves both active and previous releases.
|
||||
|
||||
@@ -4,6 +4,85 @@ This is maintainer-run operational evidence, not an independent audit or a
|
||||
general reliability claim. It records the acceptance campaign completed on
|
||||
August 14, 2026 before Tend's first public preview.
|
||||
|
||||
## Restricted multi-service campaign — August 16, 2026
|
||||
|
||||
Tend's v0.2 implementation candidate completed the same maintenance contract
|
||||
through the restricted transport for two independently configured services on
|
||||
one Linux/systemd/Caddy host.
|
||||
|
||||
### Assessed Tend implementation
|
||||
|
||||
- Implementation source commit:
|
||||
`840b77da708bbcd87a3203fb6a1f99b2984b8667`.
|
||||
- Linux/amd64 candidate binary SHA-256:
|
||||
`b3961315288871fa6085bf3b75c784a825a88f2bc3dd0694ad5d6c590eddf895`.
|
||||
- Candidate archive SHA-256:
|
||||
`948bad5271dca08b9445c387c5aea7f58f22add6b47b9071e1801a756aa71259`.
|
||||
- Toolchain: Go 1.26.6, `CGO_ENABLED=0`, `-trimpath`.
|
||||
- Trusted Gitea verification run 119 and release-candidate run 120 passed for
|
||||
the exact implementation commit. CI and an independent, network-disabled
|
||||
build each produced the same archive digest twice.
|
||||
- The installed receiver accepted only a pinned Ed25519 host key, a dedicated
|
||||
forced-command deployment key, the exact `tend-receive-v1` protocol, two
|
||||
allowlisted service names, and separately repeated artifact digests. An
|
||||
attempted arbitrary SSH command was refused.
|
||||
- Production source, Go caches, repository credentials, secret values, remote
|
||||
paths, and shell commands did not cross the transport boundary.
|
||||
|
||||
Adding this evidence changes VCS build metadata but not executable logic. The
|
||||
signed preview tag and attached release evidence therefore identify the final
|
||||
evidence-bearing candidate and its required last maintenance pass.
|
||||
|
||||
### Gamertan blue-green campaign
|
||||
|
||||
- Application source commit:
|
||||
`a7e54047d3dc11671824b6ecc8ed698a9dd04421`.
|
||||
- Preview 27 artifact SHA-256:
|
||||
`edadef3a97c089771e2b6bb7dadd58928762284878f76f23b112c61ae282f17f`.
|
||||
- The artifact was built twice, byte-identically, in the pinned Go 1.26.6
|
||||
image with networking disabled and the audited cached module graph.
|
||||
- The inactive slot passed health, readiness, page-marker, Caddy validation,
|
||||
public-origin, Sandwich Hime mount, and EQL continuity checks before traffic
|
||||
moved.
|
||||
- Explicit rollback restored preview 26; its readiness and public boundary
|
||||
passed; preview 27 was then reactivated through the recorded state.
|
||||
- Sandwich Hime's release pointer and service remained unchanged throughout.
|
||||
|
||||
### Sandwich Hime singleton campaign
|
||||
|
||||
- Application source commit:
|
||||
`435880c6751b773b6c5ee3ae6833d26e8eb7c0df`.
|
||||
- Preview 30 artifact SHA-256:
|
||||
`bee8d5bcde2c3c2e8bb96d5909a19889fc7f0be9390046c243fe20c9ff2ca44b`.
|
||||
- The artifact was built twice, byte-identically, in the same pinned,
|
||||
network-disabled Go 1.26.6 environment.
|
||||
- A transient DynamicUser candidate passed health, readiness, tutorial marker,
|
||||
canonical-origin, and Gamertan-mounted checks before the singleton pointer
|
||||
and installed service changed. The candidate port was released afterward.
|
||||
- Explicit rollback restored preview 28; its readiness and public boundary
|
||||
passed; preview 30 was then reactivated through the recorded state.
|
||||
- Both Gamertan slots, its selected Caddy upstream, and EQL remained healthy.
|
||||
|
||||
### Findings resolved by dogfood
|
||||
|
||||
The first restricted transfer stopped before artifact validation because
|
||||
`sudo` removed `SSH_ORIGINAL_COMMAND`. The forced account still refused the
|
||||
request; no service state changed. The 0BSD sudoers template now preserves only
|
||||
that one server-supplied variable, while the receiver requires its exact
|
||||
protocol value and rejects every other command.
|
||||
|
||||
The first singleton candidate stopped before pointer mutation because the
|
||||
shared environment file's live listen value overrode the candidate address.
|
||||
The site remained on its former release and retained the same process. Tend now
|
||||
rejects singleton shared environment files containing the configured listen
|
||||
key; installed units own the non-secret live address and Tend supplies only the
|
||||
transient candidate address.
|
||||
|
||||
Final verification found both service states valid, every installed unit and
|
||||
Caddy active with zero restart failures, the candidate port closed, no warning
|
||||
or error entries after the successful campaign, and representative Gamertan,
|
||||
Sandwich Hime, mounted, and EQL routes returning HTTP 200.
|
||||
|
||||
## Assessed candidate
|
||||
|
||||
- Source commit: `306d085e518cb4fe7b20a66d1e2ceb171e54ebdc`.
|
||||
|
||||
@@ -12,3 +12,8 @@ material markers. The resulting directory receives a new public root commit.
|
||||
Public Gitea is canonical for issues, contributions, and releases. GitHub is a
|
||||
read-only discovery copy of the same public tree. Tags belong only to canonical
|
||||
Gitea.
|
||||
|
||||
The snapshot includes the program, security and architecture documentation,
|
||||
copyable examples, release configuration, and local verification scripts.
|
||||
Private workflows, runner configuration, repository credentials, and raw
|
||||
operational evidence remain outside the public root.
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Schema 1 to schema 2
|
||||
|
||||
Schema 2 is intentionally not loaded as schema 1. Keep the installed v0.1 Tend
|
||||
binary available until the first schema-2 activation and rollback have both
|
||||
been exercised.
|
||||
|
||||
1. Move each configuration to `/etc/tend/services/<service>.json`.
|
||||
2. Set `schema_version` to `2`.
|
||||
3. Add `service.environment_file` below `/etc/tend/environment/`.
|
||||
4. Create that file as a regular root-owned file with mode `0600`. Move secret
|
||||
values out of JSON. Do not put the environment file in Git. For a singleton,
|
||||
keep its `listen_env` key out of this shared file: set the live address in
|
||||
the installed unit and let Tend supply only the candidate address.
|
||||
5. Set every service's `deployment.lock_file` to
|
||||
`/run/lock/tend-deploy.lock`.
|
||||
6. Add one or more query-free HTTPS `deployment.public_smoke` checks.
|
||||
7. Update installed systemd units to read the same environment file as the
|
||||
transient candidate.
|
||||
8. Install a root-owned `0600` receive policy mapping each service name to its
|
||||
exact configuration and artifact-size ceiling.
|
||||
9. Run `tend check-server` as root before accepting a transfer.
|
||||
10. Validate, activate, rollback, and reactivate one service at a time. Confirm
|
||||
unrelated services never restart.
|
||||
|
||||
Tend does not discover `.env`, infer old values, rewrite a production file, or
|
||||
silently migrate state. For local development, copy a committed `.env.example`
|
||||
to an ignored `.env.local`, restrict its mode, and load it with the application's
|
||||
own tooling. Tend never loads local dotenv files implicitly.
|
||||
+23
-13
@@ -14,26 +14,36 @@
|
||||
- Caddy configuration validates before reload.
|
||||
- An activation failure restores the previously observed state.
|
||||
- Active and previous releases survive pruning.
|
||||
- The restricted receiver accepts one versioned bounded stream, one allowlisted
|
||||
service name, and one explicitly approved digest. It accepts no remote path,
|
||||
URL, environment value, or shell expression.
|
||||
- OpenSSH host keys are pinned; forwarding, PTYs, proxy commands, local commands,
|
||||
and user SSH configuration are disabled by the client invocation.
|
||||
- Production secret values are absent from configuration, process arguments,
|
||||
artifacts, manifests, reports, deployment state, and Tend logs.
|
||||
- One host-wide lock serializes shared Caddy activation without stopping or
|
||||
reconfiguring unrelated services.
|
||||
|
||||
## Trusted inputs
|
||||
|
||||
Reviewed source, the Go toolchain, root-owned `tend.json`, systemd unit files,
|
||||
the Caddy handler template, the operator, and the target host's root account are
|
||||
trusted. Packaging a hostile repository can execute hostile Go compiler hooks
|
||||
or consume resources; Tend is not a source-code sandbox.
|
||||
Reviewed source, the pinned Go toolchain, root-owned server policy and service
|
||||
configuration, systemd unit files, the Caddy handler template, the operator,
|
||||
and the target host's root account are trusted. Packaging a hostile repository
|
||||
can execute hostile Go compiler hooks or consume resources; Tend is not a
|
||||
source-code sandbox.
|
||||
|
||||
## Adversarial inputs
|
||||
|
||||
Artifact paths and bytes, archive metadata, stale or malformed state, HTTP
|
||||
responses, subprocess failures, and filesystem objects at managed paths are
|
||||
validated and fail closed. Process output is bounded.
|
||||
Artifact paths and bytes, protocol frames, requested service names, archive
|
||||
metadata, stale or malformed state, HTTP responses, subprocess failures, and
|
||||
filesystem objects at managed paths are validated and fail closed. Process
|
||||
output and receive fields are bounded.
|
||||
|
||||
## Non-goals and preview limits
|
||||
|
||||
Tend does not defend against a concurrently malicious root user, a compromised
|
||||
kernel/toolchain/systemd/Caddy installation, denial of service by the trusted
|
||||
application, or secrets already exposed through service configuration. v0.1
|
||||
does not manage data migrations, databases, containers, Kubernetes, or
|
||||
application-specific activation. Transfer remains an application-local,
|
||||
reviewed step until a transport can be added without weakening host-key and
|
||||
credential boundaries.
|
||||
kernel/toolchain/systemd/Caddy/OpenSSH installation, denial of service by the
|
||||
trusted application, or secrets an application itself exposes. It does not
|
||||
manage data migrations, databases, containers, Kubernetes, or application-specific
|
||||
activation. EQL therefore remains outside the generic deployment adapter until
|
||||
its SQLite/catalog publication checkpoints can be modeled explicitly.
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# Two-service build, approval, and deployment
|
||||
|
||||
This walkthrough assumes one small Linux host running systemd and Caddy, two Go
|
||||
services, and a trusted Gitea build runner. The host has the same root-owned Tend
|
||||
binary at `/usr/local/bin/tend`; it does not need Git or Go.
|
||||
|
||||
## Prepare the host once
|
||||
|
||||
1. Create `/etc/tend/services`, `/etc/tend/environment`, and
|
||||
`/var/lib/tend/incoming`. The incoming and environment directories are
|
||||
root-owned mode `0700`.
|
||||
2. Install one schema-2 file per service and one root-owned mode-`0600`
|
||||
environment file per service. A singleton's shared environment file must
|
||||
not define its configured listen key; its installed unit owns the live
|
||||
address and Tend overrides only the transient candidate.
|
||||
3. Install the receive policy, forced `authorized_keys` entry, and exact sudoers
|
||||
rule from `examples/server/` after replacing every placeholder. The sudoers
|
||||
fragment preserves only `SSH_ORIGINAL_COMMAND`; the root receiver requires
|
||||
its exact protocol value and refuses every other requested command.
|
||||
4. Pin the server host key in a dedicated client file. Do not accept a new key
|
||||
interactively during deployment.
|
||||
5. Run `sudo tend check-server` and inspect the allowlisted service names.
|
||||
|
||||
## Build and approve
|
||||
|
||||
Trusted CI checks a clean pushed commit, uses a pinned Go toolchain, packages
|
||||
twice, and requires byte-identical archives. It publishes the archive,
|
||||
`RELEASE.json`, `BUILDINFO.json`, SPDX SBOM, and SHA-256 evidence.
|
||||
|
||||
The maintainer reads the candidate report and copies the exact approved digest
|
||||
into the deployment command. Tend refuses a digest that is merely inferred from
|
||||
the local file or differs from the produced value.
|
||||
|
||||
```text
|
||||
tend push --target tend-deploy@server.example \
|
||||
--known-hosts /secure/tend_known_hosts \
|
||||
--identity /secure/tend_deploy_ed25519 \
|
||||
--service example-site \
|
||||
--artifact /approved/example-site.tar.gz \
|
||||
--sha256 <produced> --approve-sha256 <reviewed> --activate
|
||||
```
|
||||
|
||||
Repeat independently with `--service docs-site`. Both builds and transfers can
|
||||
run concurrently. The host-wide lock serializes only activation because both
|
||||
services share Caddy. Tend does not stop the other application.
|
||||
|
||||
## Failure and recovery exercises
|
||||
|
||||
- Change a candidate marker: activation must fail before state is stored.
|
||||
- Make a Caddy template invalid: validation must fail and restore prior bytes.
|
||||
- Make the public marker unavailable: Tend must restore the former slot/pointer.
|
||||
- Run `tend rollback --activate` for one service and verify the other service's
|
||||
units, pointers, and public origin did not change.
|
||||
- Interrupt a transfer: no release becomes active and the incomplete incoming
|
||||
file is removed when the receiver exits.
|
||||
|
||||
After the soak, prune per service. Active and previous releases remain protected.
|
||||
Reference in New Issue
Block a user