Phase 2 — server management
This phase adds an outbound-only Ubuntu agent, one-time pairing, hardware snapshots, server status, and opt-in start/stop/restart for explicitly managed Docker containers. It does not deploy applications, execute shell commands, install Docker automatically, open inbound ports, configure tunnels, or change firewalls.
Upgrade an existing Phase 1 database
Back up the database first. Apply only the new migration with the database administrator. Do not re-run 001_foundation.sql, reset a volume, or apply migrations through the restricted API role.
docker compose --env-file .env -f infrastructure/compose.yaml exec -T postgres \
psql -v ON_ERROR_STOP=1 -U ownserva -d ownserva \
< infrastructure/migrations/003_servers.sql
docker compose --env-file .env -f infrastructure/compose.yaml up --build -d
In Windows PowerShell use Get-Content -Raw infrastructure/migrations/003_servers.sql | docker compose --env-file .env -f infrastructure/compose.yaml exec -T postgres psql -v ON_ERROR_STOP=1 -U ownserva -d ownserva. Fresh database volumes automatically apply migrations 001 and 003; 002 is the existing API-login bootstrap script. Migration numbering is intentional. A production migration ledger/runner remains future work.
The API Docker build now uses the repository root context to include packages/agent-protocol. Its database user remains the restricted ownserva_api_login.
Build and verify the agent bundle
Install Go 1.27+ and Node 22+. From the root run:
node scripts/build-agent.mjs
GO_BINARY may point to a particular Go executable. Output: dist/agent/ownserva-agent-linux-amd64, ownserva-agent-linux-arm64, install.sh, and SHA256SUMS. Builds use CGO_ENABLED=0. No official hosted release or release-signing key is provisioned by this code.
Transfer the appropriate binary, installer, and manifest to the Ubuntu server. Obtain the expected SHA-256 values for both installer and binary from an independently trusted channel (a reviewed local build or authenticated release process). A manifest downloaded next to an untrusted binary is not an independent trust anchor. Verify the installer before executing it, for example:
printf '%s install.sh\n' TRUSTED_INSTALLER_SHA256 | sha256sum -c -
Review install.sh. It requires Ubuntu 22.04, 24.04, or 26.04 LTS and amd64/arm64; other OS releases are refused. The installer installs only the verified agent and its systemd unit. Local system tools must already exist. Consult Ubuntu's release lifecycle and Docker's official Ubuntu install guide for host prerequisites.
Connect your Ubuntu server
- Sign in as an organization owner/admin. Open My Servers → Connect server, give it a name, and generate a token. Tokens expire after ten minutes, are stored only as hashes, and can be consumed once. The issuing user must still be an owner/admin at redemption. There is a ten-token-per-user/15-minute creation limit and a bounded per-TCP-peer redemption limiter.
- Configure an HTTPS origin for the Go control-plane API. TLS must terminate at a trusted proxy in front of the API, routing
/agent/v1/*unchanged. Do not expose it over plaintext public transport. The local Compose API at localhost:8080 is not reachable from a remote Ubuntu machine without your own secure control-plane hosting. No hosting gateway is provisioned in this phase. - Run the reviewed installer, replacing the binary architecture, pinned checksum, and API origin:
sudo bash install.sh \
--binary ./ownserva-agent-linux-amd64 \
--sha256 TRUSTED_BINARY_SHA256 \
--api https://api.your-control-plane.example
- Paste the pairing token at the hidden prompt. It is never passed as a process argument or written to logs. The private identity is saved in /var/lib/ownserva/agent.json with mode 0600 in a mode-0700 directory. The API stores only the public key. The installer creates an unprivileged ownserva account, a root-owned binary, a hardened systemd service, and enables it at boot.
- Confirm online in My Servers. Pairing confirms registration only; online requires an accepted heartbeat. The agent sends a heartbeat every ~30 seconds; the dashboard refreshes every 15 seconds and marks snapshots offline after 90 seconds. The first CPU sample is unavailable until a second measurement. Disk is the root filesystem; network counters sum non-loopback interfaces and can double-count virtual networking. GPU detection reports PCI identifiers, not driver/inference compatibility.
Inspect service health with systemctl status ownserva-agent and journalctl -u ownserva-agent. Retry the installer with a fresh token if pairing failed. A saved pending key can recover an identity if the initial HTTP response was lost. Do not erase a pending identity casually: a successful but unacknowledged registration may already exist.
For local development only, the direct agent CLI permits --allow-insecure-localhost with an HTTP loopback origin; non-loopback plaintext and HTTP redirects are always rejected. The production installer accepts HTTPS only. Keep the Ubuntu clock synchronized: signed requests outside ±120 seconds fail authentication.
Opt-in Docker control
Install Docker separately using its official instructions. Add --enable-docker to the installer during initial pairing to approve access to /var/run/docker.sock. This explicitly adds the agent user to the docker group: that socket permission is root-equivalent despite the service sandbox. The socket is never exposed over a network. Without approval, hardware reporting works and Docker is reported disabled.
Only containers labeled with both ownserva.managed=true and ownserva.server_id=<paired server UUID> appear and can be controlled. Label a test container explicitly, for example (creates a container on your host):
docker run -d --name ownserva-test \
--label ownserva.managed=true \
--label ownserva.server_id=YOUR_SERVER_UUID \
nginx:stable-alpine
No host port is published by that example. The agent re-inspects labels immediately before a mutation; container names, arbitrary Docker endpoints, exec, creation, deletion, volume operations, and arbitrary shell commands are not accepted. Phase 3 will add controlled application creation/builds.
Owners/admins/developers may queue start, stop, and restart on an online server. Viewers can read snapshots and history only. The UI requires confirmation and shows pending, running, succeeded, failed, expired, or unknown. Success requires a successful Docker response and a verifying inspect (including a changed StartedAt for restart). Timeouts or unverifiable outcomes report unknown, never assumed success.
Commands expire after ten minutes and are dispatched at most once. An in-flight command blocks another command for the same container. A durable local journal is written before execution; after restart, incomplete work is reported unknown and is not executed again. Unconfirmed dispatched work becomes unknown after five minutes. A command lost between server-side dispatch and agent receipt is also unknown, not automatically redelivered. Result acknowledgments retry idempotently. Command polling is a persisted PostgreSQL queue in this phase; Redis/Asynq workers are deferred to deployment builds.
Revoke and re-pair
Owners/admins can revoke an agent from its server detail view. The API immediately refuses new requests for its key; pending commands expire and in-flight outcomes become unknown. Local containers keep running. On HTTP 401 the service exits with status 77 and systemd does not restart it. A rejected signature can also indicate clock skew, so check time synchronization before replacing an identity.
To re-pair intentionally: stop the service, retain a private backup if needed, archive the entire /var/lib/ownserva state directory (including the command journal), generate a fresh pairing token, and rerun the installer. Revocation is permanent; re-pairing creates a new server record. This release has no automatic key rotation or agent auto-update. To upgrade the binary, verify a new bundle and rerun the installer; restart the service explicitly after installing the new binary.
Security boundary and API
Agent requests sign the protocol version, method, escaped path, timestamp, random 32-byte nonce, and SHA-256 body hash with Ed25519. The API checks the public key, revocation, timestamp, and signature, then atomically stores the nonce before executing a handler. Nonces are retained for five minutes; expired signatures remain invalid after cleanup. Fresh keys are unique and pairing is atomic. API credentials cannot read private keys because private keys never leave the Ubuntu host.
Agent transactions set app.agent_server_id/app.agent_key_id locally; user transactions set app.user_id locally. RLS scopes snapshots and commands, independently of handler predicates. Only narrowly scoped SECURITY DEFINER functions may consume a pairing token or look up an active public key. Their PUBLIC execution grant is revoked. Agent endpoints do not accept browser session cookies. Browser mutations retain exact-origin checks and independent Go session/membership/role checks.
New user routes: GET /v1/organizations/{id}/servers, POST /v1/organizations/{id}/pairing-tokens, GET/POST /v1/servers/{id}/commands, POST /v1/servers/{id}/revoke. Agent routes: POST /agent/v1/pair, GET /agent/v1/identity, POST /agent/v1/heartbeat, POST /agent/v1/commands/next, POST /agent/v1/commands/{id}/result. See docs/openapi-phase2.yaml.
Verification limits
Automated tests exercise real embedded PostgreSQL migrations/RLS, single-use/expired pairing tokens, role denials, credential revocation, nonce uniqueness, and one-time command dispatch. Go tests cover signatures and tampering, command validation, resource parsers, HTTPS restrictions, corrupt state/journals, Docker label checks, and verified outcomes using a fake Docker HTTP transport. Linux amd64 and arm64 builds are generated.
This Windows workspace has no runnable Ubuntu distribution or Docker daemon. Actual systemd installation, Linux hardware sampling, Docker socket operations, and remote HTTPS pairing must therefore be acceptance-tested on an Ubuntu host before treating this release as production-ready. Cross-compilation and mocked Docker tests are not that acceptance test.