Ajo Server

The host

Domains, secrets, backups and the privilege model of an Ajo Server host.

Once a host has booted and its administration App is running, most work happens in two places: the admin in the browser, and host programs that act on typed requests. This page explains how those pieces fit, how Apps get custom domains, secrets and backups, and which privileges each part holds.

How the host is organized

PartResponsibility
HostImage buildAuthenticates its inputs against a reviewed Alpine base and package lock, assembles the root in isolated mount and network namespaces from a private snapshot of declared source files, and inspects it before publishing a release.
First bootReads the host name and owner SSH key from a fixed set of provider metadata, creates the host identity, grows the filesystem and configures ingress. Identity persists across reboots, and access stays closed when a required step fails.
Deployment supervisorImports an archive, from SSH or from a verified upload, gives the version a stable tag and launches it. It journals each replacement, checks readiness and attempts rollback.
LauncherOwns each App’s mounts, environment, ports and resource containment, and gives every App its own data directory.
State projectionsHost programs publish versioned records under /var/lib/ajo-state. Apps receive read-only mounts of them, never a Podman socket or arbitrary host command execution.
Typed operationsThe admin’s optional operations mount carries a FIFO. Lifecycle requests use a closed set of verbs: start, stop, restart, remove, activate and update. Deployments, domains and secrets have their own schemas.
Administration AppHost inventory and metrics, App controls, versions and logs, passkey sign-in, invitations and scoped team access, mail alerts, security updates, domains and secrets.

Submitted is not done

An accepted HTTP request only means an operation was submitted. Every request carries an ID, and the admin waits for the host result with the same ID. When none arrives in time, it reports the outcome as unconfirmed rather than retrying. An absent receipt is not evidence of failure, of success, or of permission to delete anything. Security updates follow the same pattern and may ask for a reboot.

App containment

The launcher places each App inside its resource limits before its entrypoint runs; a separate sweep verifies that state afterwards. Apps listen on loopback behind nginx. The host manages names and TLS and rejects unknown host names. Never replace the launcher with an ad-hoc Podman command: it would skip the containment, mounts and environment policy.

Custom domains

Every App keeps its generated https://<app>.<host> address. A production App can add up to eight exact custom domains alongside it, and each address keeps its own URL; no canonical redirect is imposed. A host admits at most 152 custom registrations in total. The host namespace, staging, previews and platform workloads are reserved, and www is a separate registration rather than an automatic alias.

  1. Open the Domains tab in the App’s Settings. It requires the Owner role, meaning a browser session with the global config:manage grant; Bearer credentials are refused, even beside a cookie, so a deployment token cannot configure domains. Add the domain.

  2. Publish a TXT record named _ajo.<domain> with the value ajo-domain=<challenge>. The complete value must be in the first TXT string. The challenge is case-sensitive and expires after one hour; Get TXT asks the host to renew an expired one.

  3. Point the domain’s A or AAAA records at the host. Domain operations never create or change provider DNS records, and the App never receives DNS provider credentials.

  4. Deploy the App with current ajo-kit, ajo-kit-auth and ajo-kit-server packages, so its artifact supports the host-managed origin manifest.

  5. Choose Verify domain. Only a confirmed result and an active projection establish activation; missing DNS, failed certificate issuance or an unconfirmed reload do not. update_required means the running App did not acknowledge multi-origin support: rebuild, redeploy and verify again.

Origins and CSRF

The host mounts the App’s origin directory read-only at /ajo/origin and sets AJO_ORIGINS_FILE=/ajo/origin/origins.json. The manifest holds the generated address and every verified alias, including the canonical APP_URL. The App admits the direct Host header against this list before it serves routes or assets.

  • Browser writes must match the address currently in use, so two admitted aliases do not allow cross-alias CSRF.
  • Forwarded headers do not expand the list.
  • Links the App generates still use APP_URL. Cookies and the passkey relying-party scope are not broadened by adding a domain, so a session does not transfer to an unrelated domain.
  • The acknowledgement the host checks during activation identifies runtime support. It is not cryptographic attestation or continuous monitoring.

Removing a domain

Removal requires typing the exact domain and leaves the other addresses, data, images and environment values in place. The host refuses to remove the App’s canonical APP_URL, even while the App is stopped. To retire that address, the operator first changes the App’s environment file and deploys the replacement.

App secrets

The Secrets tab in the App’s Settings replaces its environment. It has the same requirements as Domains: an Owner with a current browser session and config:manage, CSRF protection, and no Bearer credentials.

  • Each submission is the complete desired set of names and values. Names omitted from it are removed.
  • Values are write-only. They stay in host custody in a mode-0600 file; views, receipts and live updates show names, revision and status, never values or digests.
  • APP_URL stays operator-owned. It changes through normal deployment, not through Secrets or by adding a domain.
  • Submit once and wait for the result correlated to that submission. A submitted or unconfirmed result is not completion. After a terminal success, confirm the applied revision and the list of names.

If a replacement is interrupted, recovery either keeps the container known to be ready or finishes the new one before publishing a single applied revision. Do not edit journals, receipts or retained containers by hand.

Backup layers

There are three separate facilities, and each proves something different.

LayerPurposeEvidence it providesWhat it does not prove
Admin backup artifactExport and verify the admin’s accounts database.Integrity, hash and table counts on the copy.Anything about other Apps’ data. The copy stays on the host until you transfer it and verify it there.
Host App-data archivesLocal archives of App data, and restoration from them.Verified local archives and a restore path.Protection against losing the host.
Backup coordinatorEncrypted SQLite snapshots and page deltas, uploaded to a remote destination.Remote readback of immutable units.That a restore succeeds, or that a whole deployed system can be recovered.

The coordinator is its own workload. It needs every App’s database and a private signing and spool directory, so it stays outside the HTTP admin, which also lets capture continue while the admin is redeployed. Its remote adapter uses Google Drive with the per-file drive.file scope. Supported packaging for the coordinator and a complete host-loss recovery drill are still open work.

Privilege model

Ajo Server is a single-host system operated by a trusted owner. Rootless containers and resource limits reduce exposure; they are not a guarantee for arbitrary hostile multi-tenant workloads.

ActorAuthority
Host owner, ajo-opsRoot-equivalent administration through passwordless doas.
Ordinary AppIts own data, its declared engine capabilities and the projections mounted into it.
Administration AppPlatform projections, its accounts database and the optional typed operations FIFO.
Backup coordinatorEvery App database, plus a private spool and signing directory.
HostImage inputsReviewed release and package trust roots, exact versions and hashes.
  • Rootless does not make the admin unprivileged. Its platform mounts and operations capability are broader than a normal tenant’s, so a compromised admin is not equivalent to a compromised App.
  • Containers share the operator’s rootless mapping. Bind mounts and file ownership are part of the isolation contract; different container names do not imply different host identities.
  • Permissions stay narrow. A deployment token names one exact App and is checked against the account’s current grants on every request. Deploy permission grants no lifecycle, configuration, people or restore powers.
  • Fresh images only. Cloning an already booted disk can duplicate host identities and is not a safe provisioning shortcut.

Do not publish secrets or exploit details in a public issue. The project does not currently advertise a dedicated security-reporting address or a production support commitment.

Operator commands

Building the image, configuring and deploying the administration App, domain requests over SSH, diagnosing an operation, applying security fixes and exporting backups all have exact commands. They are deliberately not reproduced here: most run with root-equivalent authority, and the operations guide keeps each one next to its preconditions and failure handling. Follow it directly:

  • Operations: build, provision, deploy, diagnose and recover.
  • Security: the full privilege model and its limits.
  • Roadmap: acceptance status and remaining work.

Source of truth: ajo-server docs/architecture.md, security.md and operations.md