9.7 KiB
Qortal preparation components — not a catalogue-ready package
This directory contains startup validation and a locally buildable candidate image, not an installable manifest, published image or accepted app entry. Full package, first boot, lifecycle, credential, provenance and operator gates remain. Bounded initialized-fixture API evidence is described separately below.
The initial packaging proposal is Gitea PR164 at
f115478fdfd7f561fed4f48d46333b6b204f7253, subsequently updated to
4cc625e4d1aebe57fece3ef5bb8e70f40b372732. The image/nginx/status page are
selectively derived from that proposal, retaining the stronger guard here.
Acceptance/replacement must follow
Archipelago's canonical ngit workflow. Its generic manifest-selected credential
reader is deliberately not included: declaring a shared generated-secret
filename does not establish exclusive app ownership.
Startup contract
start.sh expects Python 3 and check-settings.py installed as
/usr/local/bin/qortal-check-settings, the pinned jar at
/usr/local/qortal/qortal.jar, and the companion nginx configuration at
/etc/qortal-ui/nginx.conf. The Dockerfile supplies these prerequisites and
pins the Temurin base, official JAR and upstream logging configuration. Build
locally with podman build -t localhost/archipelago-qortal:6.1.9-archy.1 apps/qortal.
The logging file is explicitly selected from the read-only image.
The operator's /qortal/settings.json is never repaired or overwritten. It must
be a regular, non-symlink file containing strict UTF-8 JSON, at most 1 MiB, with
unique decoded property names. Core accepts some additional comment/trailing
comma syntax; this candidate intentionally refuses those extensions rather than
risk checking a different interpretation. Any existing settings conversion
requires explicit review and preservation of the original, not an automatic
migration.
Required explicit booleans:
{
"localAuthBypassEnabled": false,
"apiRestricted": true,
"autoUpdateEnabled": false
}
Other ordinary settings are retained. userPath, apiKey and apiKeyPath are
refused even if null: settings redirection or overriding the managed key source
would invalidate the startup check. Auto-update is disabled for pinned package
provenance; missing flags fail closed rather than silently modifying policy.
apiRestricted is not a substitute for route-by-route API-key authorization.
The helper writes a private, exclusive-created runtime snapshot in a fresh mode-0700 temporary directory. Only that snapshot is passed to Core. Edits to the persistent source after validation cannot change this launch's document. Snapshot files are removed after both children stop. SIGTERM/SIGINT are forwarded; the wrapper waits for Core and UI cleanup and preserves the stop exit status.
QORTAL_API_KEY must contain the platform-generated 32-byte hexadecimal key.
A new apikey.txt is created mode 0600 without a newline. An existing regular
file must be private and match exactly; missing/malformed input, symlinks, FIFOs,
exposed permissions or conflicts are refused. The wrapper does not rotate, trim, repair or replace an existing
key to make startup succeed. Conflicts require explicit recovery. The environment
secret is removed before either child launches, and failures do not print values.
Evidence and remaining exits
test_settings.py: eleven tests with multiple adversarial cases, run in a secret-free, network-none, read-only disposable Python container.- Actual official Qortal 6.1.9 jar parser paired with the new entrypoint in a bounded disposable JRE/Python image: safe snapshot interpretation, original escaped-name regressions, duplicate keys, redirection, environment-key removal, key preservation, child cleanup and TERM behavior pass.
- The initial entrypoint/parser probe used stub children; it did not qualify the actual daemon. Subsequent checks below run the actual JAR and nginx.
The source jar's SHA256 was verified as
a5fbe763645a53995565c9cb93491e024384431ee29eee38a4146b6a13ed2e87, matching the
official release asset and proposal pin. No daemon/network/funds were launched
by the parser test. Images used for tests are not release artifacts.
Package-side empty-cache overlay prerequisite
prepare-runtime-jar.py is the package entrypoint boundary for the accepted
empty-cache behavior. It never replaces /usr/local/qortal/qortal.jar. It reads
the official JAR and replacement class as bounded no-follow regular-file
snapshots, verifies both against empty-cache-overlay-contract.json, and creates
an exclusive runtime JAR plus rollback receipt in the private runtime directory.
It rejects duplicate/missing target entries, signed JARs, changed bytes,
noncanonical contract fields, existing outputs and rollback byte/path drift.
start.sh runs this preparation before settings, credentials, nginx or Core and
launches only the verified runtime JAR.
The checked-in contract deliberately records upstream_provenance_status as
pending and has no provenance receipt hash. The Dockerfile therefore copies
the helper and contract but does not embed the retained test-only class.
Until an independent upstream/package provenance decision supplies and reviews
that exact class and changes the contract atomically, startup refuses with exit
78. The retained fixture JAR and class under historical evidence are not silently
promoted into package inputs.
Rollback verifies the unchanged official JAR and exact prepared runtime bytes
before deleting only the runtime JAR and its receipt. Operator /qortal data is
outside this helper. Credential drift remains separately fail-closed through
qortal-check-settings; if an existing API key conflicts, the newly created
settings snapshot is removed and neither key nor operator data is rewritten.
Focused tests exercise nonempty state preservation, prepare/rollback, missing or
changed official JAR, changed class, pending provenance, changed runtime rollback
refusal and credential conflict through the production helpers.
Actual image fixture — 2026-10-21
tests/apps/qortal/run-disposable.sh IMAGE initialized-fixture passes with the
actual Core/nginx processes, nonroot UID/GID 1000:100, network-none, read-only
root and disposable storage. It checks missing/wrong API-key denial on the real
GET /admin/stop route (403), correct-key stop (200), restricted nginx proxy
routes, private managed-key bytes/mode/inode, absence of the environment secret
from both children, operator-settings/sentinel preservation across recreation,
and refusal of a changed secret without rotation or disclosure. No live node,
wallet, peers or funds are involved.
This deliberately does not qualify fresh genesis or bootstrap. With
bootstrap=false, the official 6.1.9 JAR fails before API startup with
statement is not in batch mode during empty QDN-cache initialization. Default
fresh mode preserves that failing reproduction. The separate initialized
fixture uses the actual pinned Core genesis processor, commits the empty-cache
metadata flag, and uses bootstrap=true, archiveEnabled=false in synthetic
settings to avoid the upstream height-below-three rebuild. It invents no chain
transactions or signatures; it is nevertheless an explicit prepared database,
not a fix, installer migration, first-boot pass or bootstrap qualification.
Never use InitializedGenesis.java against operator data.
The image's local tag is not yet admitted by the fixed credential reader; the same-origin privileged credential boundary is also unresolved. No generic credential schema or catalogue manifest is activated here.
Empty-cache candidate — 2026-10-22
A narrow unpublished test-only source patch is retained at
tests/apps/qortal/empty-cache.patch. It skips executeBatch() only when no
signature entries were added; normal lower-case cache population, completion
flag, transaction commit and nonempty batch updates remain unchanged. It does
not preseed a database or bypass startup validation.
EmptyCacheRegression.java runs the actual pinned HSQLDB and cache manager
against an in-memory synthetic schema. The original class reproduces the empty
batch error. The patched class passes empty initialization, idempotence and
nonempty latest-signature selection/preservation. No Core or wallet is started
by this focused probe.
make-cache-fixture.py creates an exclusive new test JAR from the exact original
SHA256, refuses signed or ambiguous originals, and verifies that exactly one
class entry differs. It does not overwrite the original or publish a package.
The candidate overlay SHA256 is
252b6c42a1ec2e7f35b879676650e44579a8f221665a7a69e421a2a5cba7581a.
With that overlay, run-disposable.sh IMAGE fresh passes actual Core/nginx fresh
startup, API-key negatives, restricted UI proxy, graceful stop/recreation,
private-key/settings preservation and changed-secret refusal. This used an
empty named disposable volume, not InitializedGenesis.java, with external
network disabled, 2 GiB memory and zero swap allowance. To supply a test overlay,
set QORTAL_FIXTURE_JAR and its exact QORTAL_FIXTURE_JAR_SHA256; neither modifies
the image or installed data.
This does NOT make the original official JAR fixed. Source/provenance and independent review, clean compatible packaging, nonempty upgrade/rollback, network/bootstrap/gateway and installed-node qualification remain required. No production Dockerfile, manifest or catalog entry is switched to this overlay.
Still required: independent security review, a bounded credential card or actual app-owned secret design, sanitized manifest/image/UI integration, API denial and Hub/gateway checks, health truthfulness, sizing, upgrade/backup/recovery/rollback, package/source acceptance and signed catalogue admission. Do not deploy these components as a completed app or overwrite a live operator configuration.