Add signed passive runtime updates and independent crash recovery
This commit is contained in:
+143
@@ -0,0 +1,143 @@
|
||||
# Signed Hydra runtime updates
|
||||
|
||||
The new ISO includes an independent updater and an independently pinned Ed25519
|
||||
public key. Discovery uses the public Gitea repository at
|
||||
`https://git.shanaty.ru/Emil_Shanaty/Hydra-OS`; discovery is not a trust source.
|
||||
An empty releases list is normal and reports no update.
|
||||
|
||||
```sh
|
||||
hydra update check
|
||||
hydra update status
|
||||
sudo hydra update apply
|
||||
sudo hydra update rollback TRANSACTION
|
||||
sudo hydra-update-recover
|
||||
```
|
||||
|
||||
`check` and `status` run without sudo. Apply prints the transaction identifier.
|
||||
After applying or rolling back, log out of the graphical session and log in again
|
||||
to load the changed runtime. Existing guest disks and profiles remain unchanged.
|
||||
If the normal `hydra` command is broken, use the independent `hydra-update` command
|
||||
or `hydra-update-recover` directly. A prepared/interrupted transaction blocks the
|
||||
next apply until recovery succeeds. Recovery may be repeated after interruption.
|
||||
|
||||
Only the latest current transaction can be explicitly rolled back. Rollback first
|
||||
checks that its installed files still match their recorded content, ownership and
|
||||
permissions, and refuses local edits. Recovery restores the originals of an
|
||||
interrupted transaction without requiring mixed runtime files to match the new
|
||||
release. Corrupt or missing backups stop restoration before any destination changes;
|
||||
keep the complete transaction directory for inspection. No automatic cleanup removes
|
||||
failed transaction evidence.
|
||||
|
||||
Runtime updates contain only Python runtime modules, `usr/lib/hydra/bin/hydra`,
|
||||
`etc/motd`, the Fastfetch configuration and its text logo. They cannot contain
|
||||
`update*.py`, the independent updater, release keys, `/home`, guest profiles/disks,
|
||||
credentials, network configuration, services, Portage, the kernel or boot files.
|
||||
Runtime rollback **does not roll back Gentoo or its kernel**. Whole-system recovery
|
||||
is a separate facility with separate acceptance requirements.
|
||||
|
||||
## Trust bootstrap and installation
|
||||
|
||||
These files are installed by the trusted image build, outside the runtime payload:
|
||||
|
||||
| Source | Installed path | Owner/mode |
|
||||
| --- | --- | --- |
|
||||
| `hydra/update_engine.py` | `/usr/lib/hydra-update/engine.py` | root:root 0644 |
|
||||
| `bin/hydra-update` | `/usr/bin/hydra-update` | root:root 0755 |
|
||||
| `bin/hydra-update-recover` | `/usr/bin/hydra-update-recover` | root:root 0755 |
|
||||
| Independently supplied public key | `/etc/hydra/update.pub` | root:root 0644 |
|
||||
|
||||
The launchers use isolated Python (`/usr/bin/python3 -I`) and never import the
|
||||
mutable `hydra` package. Install Python and OpenSSL in the image. Protect all parent
|
||||
directories with root ownership and no group/other write permissions. The engine
|
||||
and its key cannot update themselves through this protocol; their maintenance is
|
||||
an independently verified image/bootstrap operation.
|
||||
|
||||
An old ISO without these files cannot safely bootstrap by executing a downloaded
|
||||
zipapp. Reinstall from the newly verified ISO, or independently verify and install
|
||||
the exact engine, launchers and public key from a trusted source as administrator.
|
||||
Do not fetch the trust key from the same release discovery response being verified.
|
||||
Verify its fingerprint through the independently supplied release information.
|
||||
|
||||
Production mutation requires UID 0, x86_64, the Hydra build marker and Hydra's
|
||||
`ID=hydra-os` OS release. Production always uses `/etc/hydra/update.pub`; a key
|
||||
override is refused. A `--root DIRECTORY` support/testing argument targets an
|
||||
explicit isolated root containing the build marker and existing payload parent
|
||||
directories. Only these staging roots allow `--public-key FILE`. No environment
|
||||
variable silently redirects an installed update to another root or key.
|
||||
|
||||
## Release creation
|
||||
|
||||
Keep the Ed25519 private key outside Git, the source/build context and the Nix
|
||||
store, in a user-owned private file (0600). Never upload it as a release asset.
|
||||
Provision this key through the release operator's separate key-management process.
|
||||
The builder does not generate keys, publish assets or overwrite an existing output.
|
||||
|
||||
```sh
|
||||
python3 dev/release.py \
|
||||
--source /path/to/verified-checkout \
|
||||
--output /path/to/new-release-directory \
|
||||
--private-key /private/outside-git/release.pem \
|
||||
--version 1.0.0 --sequence 1
|
||||
```
|
||||
|
||||
The three release assets are `manifest.json`, `manifest.sig`, and `runtime.zip`.
|
||||
Publish them under the immutable Gitea tag `1.0.0`, exactly matching the signed
|
||||
version. Each new release needs a strictly increasing integer sequence; never
|
||||
reuse a sequence or mutate assets at an existing version. Rollback retains the
|
||||
highest accepted sequence independently of the current installed version.
|
||||
The builder is deterministic for identical source, version, sequence, key and
|
||||
compression implementation. It records Git HEAD; build from a reviewed clean
|
||||
checkout so that commit identifies the bytes actually being released.
|
||||
|
||||
The signature is Ed25519 over the exact bytes of `manifest.json`, using OpenSSL
|
||||
`pkeyutl -sign -rawin -inkey PRIVATE -in manifest.json -out manifest.sig`.
|
||||
Verification uses `pkeyutl -verify -pubin -rawin` with the pinned key. No downloaded
|
||||
Python is executed. `runtime.zip` is passive file data with reproducible timestamps.
|
||||
|
||||
Manifest format 1 has exact top-level fields `format`, `product` (`Hydra OS`),
|
||||
`scope` (`runtime`), `channel` (`stable`), `architecture` (`x86_64`), `sequence`,
|
||||
`version`, `source_commit`, `archive` and `files`. The archive entry contains
|
||||
`name`, `size` and `sha256`; each allowed file has `size`, `sha256` and `mode`
|
||||
(0644 or 0755). Duplicate keys, unknown fields, unsafe paths, links/special archive
|
||||
members, mismatched hashes/sizes and unsupported formats are rejected.
|
||||
The maximum manifest is 256 KiB, archive/expanded payload 16 MiB, individual file
|
||||
4 MiB and file count 256. Downloads have a 15-second socket timeout and 60-second
|
||||
per-artifact deadline; redirects remain on the fixed HTTPS origin. Discovery
|
||||
checks up to the latest 20 published release records, skipping draft/prerelease
|
||||
records. Publish runtime releases regularly enough that the current stable release
|
||||
remains in that window.
|
||||
|
||||
For offline/local verification and application:
|
||||
|
||||
```sh
|
||||
hydra update check --artifacts /trusted/local/release
|
||||
sudo hydra update apply --artifacts /trusted/local/release
|
||||
```
|
||||
|
||||
These use the same pinned key and validation as online releases. For developer
|
||||
staging, add `--root /isolated/hydra-root --public-key /test/public.pem`.
|
||||
The Python API is `Updater(root, public_key).apply(artifacts)`, `.status()`,
|
||||
`.rollback(transaction)` and `.recover()`; staging roots may run as their owner.
|
||||
|
||||
## Durability and limits
|
||||
|
||||
A nonblocking `flock` serializes mutations. The private state directory is
|
||||
`/var/lib/hydra-update` (0700). Each transaction saves original regular-file bytes,
|
||||
hashes, sizes, permissions and ownership; symlink targets/ownership and previously
|
||||
absent files are recorded explicitly. Unsupported destination types and unsafe
|
||||
parent directories are refused. Payload parent directories must already exist.
|
||||
|
||||
All destination paths and payload bytes are checked before creating update state.
|
||||
All backups and a prepared journal are durable before the first runtime replacement.
|
||||
Writes use adjacent temporary files, file fsync, atomic replacement and directory
|
||||
fsync. Recovery is independent of the runtime package. The public read-only status
|
||||
summary is `/usr/share/hydra/update-status.json`; private journals are authoritative,
|
||||
and an abrupt loss of power may leave the summary briefly older than those journals.
|
||||
Run recovery as administrator when an update was interrupted.
|
||||
|
||||
This is a per-file recoverable transaction, not a simultaneous filesystem snapshot.
|
||||
A running process can observe a mixture during apply. Do not launch new Hydra
|
||||
runtime operations until apply/recovery finishes. Root administrators and processes
|
||||
that can already modify protected root-owned directories are outside the updater's
|
||||
concurrent-write threat model. Filesystem/device failures can require manual repair;
|
||||
recovery retains all evidence and never promises recovery from lost backup storage.
|
||||
Reference in New Issue
Block a user