Policy Server Deployment and Operation¶
aigo-policy-server is the central serving spine for an organization's signed enterprise policy. It holds the organization's private Ed25519 signing key, serves the signed policy that every desktop and headless client verifies with only the public trust anchor, and handles device enrollment.
The policy server is server-side and private. It is a deliberately separate artifact from aigo-server (the inference server) and from the desktop app, and it is not bundled with the client. Keeping the signing key and the authored policies out of the artifact that ships to every user and inference node is least privilege: the signing key is the root of trust, so it lives only on the policy server you operate.
For the policy document schema (what a policy may target, lock, and gate), see the Policy Schema Reference. This page covers running the server.
What the policy server does¶
The server has one job split across four route families:
- It validates a one-time enrollment token and returns the public trust anchors plus polling metadata, so a new device can verify future policies.
- It serves the current signed policy.
- It serves a cheap policy id plus issue date for poll-without-download.
- It accepts signed client audit batches.
The control plane also exposes the minimum operator surface needed to protect those routes: first-run administrator registration, username/password sessions, and API-key management. It has no policy-write API. The policy and enrollment-token documents are read at process startup, and an operator updates them by validating the new files and restarting the service.
It does not run the inference runtime, the continuum-router, or any model loader. It reuses the shared aigo-policy crate for the policy schema and Ed25519 signing, and the same authenticated REST stack as the headless server.
Install the binary¶
The release pipeline publishes bare Linux x86-64 and ARM64 binaries to the private lablup/backend.ai-go release only. No policy-server asset is copied to the public release repository, desktop installer, or headless inference-server package. The download helper requires gh to be authenticated with read access to the private repository, downloads the version pinned for this checkout, and verifies the matching SHA-256 sidecar:
gh auth status
./scripts/download-aigo-policy-server.sh \
--linux-x64 \
--dest ./dist-policy-server
sudo install -m 0755 \
./dist-policy-server/aigo-policy-server-linux-x64 \
/usr/local/bin/aigo-policy-server
aigo-policy-server --version
Use --linux-arm64 on an ARM64 management host. The script also accepts --version VERSION; otherwise it downloads the release pinned in the script.
To build from a source checkout instead:
cd src-tauri
cargo build --release \
-p aigo-policy-server \
--no-default-features \
--features policy-server \
--bin aigo-policy-server
The resulting binary is src-tauri/target/release/aigo-policy-server relative to the repository root. The policy-server feature is intentionally separate from the desktop and headless products.
Prerequisites¶
Before standing up the server, plan the following.
- A host reachable by your managed devices over HTTPS (terminate TLS at a reverse proxy in front of the server, or on the network path).
- A secure location for the private signing key, with access limited to the operators who manage policy.
- An authored
EnterprisePolicyJSON document (see the Policy Schema Reference). - A persistent data directory for administrator accounts, access keys, and the device registry.
Generate the signing key¶
The signing key is a PKCS#8 v2 Ed25519 key. Generate one under an owner-only umask and store it securely:
The key is printed as base64 to stdout (the human-readable notice goes to stderr, so the redirect captures only the key). This is the root of trust for your fleet. Store it in a secrets manager or an access-controlled file, back it up, and never commit it or place it on a client.
You may also keep the key as a raw binary PKCS#8 file. The server accepts either a raw PKCS#8 document or its base64 text encoding from --signing-key-file.
Extract and distribute the trust anchor¶
Clients verify the signed policy with the public trust anchor only. Print it from the signing key:
aigo-policy-server \
--signing-key-file policy-signing.pk8.b64 \
--anchor-id org-2026 \
--print-anchor \
> trust-anchor.json
This prints a JSON trust anchor (an id and a base64 public key). The --anchor-id you choose is recorded in the served policy's anchorId and must match the anchor id you provision into clients.
You distribute this public anchor to devices inside the provisioning profile (trustAnchors). When a device enrolls against this server, the server returns the same anchor derived from this key, so a freshly enrolled device can verify the policy it then fetches. See Device Enrollment and Trust Anchors.
Author and sign the policy¶
Write your EnterprisePolicy document (for example /etc/aigo/policy.json) following the Policy Schema Reference. The server can sign it at startup, or serve an already-signed document verbatim.
A minimal policy needs a schema version, a unique policy id, an issue time, and the two required egress flags. This example also disables and locks remote access:
{
"schemaVersion": 1,
"policyId": "acme-baseline-2026-09-01-01",
"issuedAt": "2026-09-01T00:00:00Z",
"egress": {
"enforce": false,
"audit": true
},
"settingsOverrides": {
"advanced": {
"enableRemoteAccess": false
}
},
"lockedPaths": [
"advanced.enableRemoteAccess"
]
}
Sign an authored policy at startup with the signing key:
aigo-policy-server \
--signing-key-file /etc/aigo/policy-signing.pk8 \
--policy-file /etc/aigo/policy.json \
--anchor-id org-2026
Serve a pre-signed SignedPolicy document verbatim (no signing key needed at runtime, useful when signing happens on a separate, more isolated host):
--policy-file and --signed-policy-file are mutually exclusive. The signing key may also be supplied through AIGO_POLICY_SIGNING_KEY (base64) or AIGO_POLICY_SIGNING_KEY_FILE. It is never read from the wire.
Validate the complete startup input without opening a listener with --dry-run. It confirms that the policy parses, its schema is supported, the key signs, enrollment-token inputs are valid, the shared server configuration is valid, and the data directory can be created:
aigo-policy-server \
--host 127.0.0.1 \
--port 8444 \
--data-dir /var/lib/aigo-policy-server \
--signing-key-file /etc/aigo/policy-signing.pk8 \
--policy-file /etc/aigo/policy.json \
--anchor-id org-2026 \
--enrollment-tokens-file /etc/aigo/enrollment-tokens.json \
--dry-run
The authored policy is read and signed once at startup. Replacing policy.json while the process is running does not change the served policy. For every update, assign a new policyId, update issuedAt, run --dry-run, install the file, and restart the server. Clients use policyId as the version key and skip the full download while it remains unchanged, even if other fields or issuedAt changed.
Configure device enrollment¶
Enrollment is offered only when you configure one or more one-time tokens. Each token is single-use: it is consumed on the first successful enrollment and cannot be reused. The token document maps each token to an optional organization label.
--enrollment-token was removed
Earlier versions accepted --enrollment-token TOKEN[:ORG] on the command line. That flag is gone. An enrollment token is a bearer secret, and on Unix every process argument is readable by any local account through /proc/<pid>/cmdline, ps, shell history, and container inspect output, so passing one that way published it. The server still recognizes the flag and refuses to start with a message naming the replacements below, so an old init script fails loudly instead of quietly serving without enrollment.
Supply a JSON document mapping tokens to organization labels (null binds without an org) through a file, the environment, or standard input. Tokens are never logged in clear:
chmod 600 /etc/aigo/enrollment-tokens.json
aigo-policy-server \
--signing-key-file /etc/aigo/policy-signing.pk8 \
--policy-file /etc/aigo/policy.json \
--anchor-id org-2026 \
--enrollment-tokens-file /etc/aigo/enrollment-tokens.json \
--poll-interval-secs 900
The file must not be readable beyond its owner. A group- or world-readable token file (or signing key) stops startup rather than producing a warning, because an operator who misses the warning keeps serving with an exposed secret.
To keep tokens off the filesystem entirely, pass the same document in AIGO_POLICY_ENROLLMENT_TOKENS, or pipe it on standard input with --enrollment-tokens-stdin:
vault kv get -field=tokens secret/aigo/enrollment \
| aigo-policy-server \
--signing-key-file /etc/aigo/policy-signing.pk8 \
--policy-file /etc/aigo/policy.json \
--anchor-id org-2026 \
--enrollment-tokens-stdin
All three sources merge, in the order file, environment, standard input. They are read once at startup. After adding or removing tokens, restart the service before handing a new token to a device.
Consumed tokens and enrolled devices are recorded under the data directory in device-registry.json, written atomically at mode 0600. A restart therefore neither makes an already-used token valid again nor loses the record of an enrolled device. Consumed tokens are stored as SHA-256 digests, so the file is not a second copy of your token list. Keep the whole data directory on persistent storage and include it in the server's backup and recovery plan. It also holds users.db and the encrypted access-key store.
The poll interval you set (--poll-interval-secs, default 900) is returned to the device and controls how often it polls for an updated policy. When no enrollment tokens are configured, enrollment is simply not offered and POST /api/v1/enroll returns 503.
Start the server and bootstrap operator access¶
Use an explicit persistent data directory. For a reverse proxy on the same host, bind the policy server to loopback and keep the proxy disabled until the first administrator exists:
aigo-policy-server \
--host 127.0.0.1 \
--port 8444 \
--data-dir /var/lib/aigo-policy-server \
--signing-key-file /etc/aigo/policy-signing.pk8 \
--policy-file /etc/aigo/policy.json \
--anchor-id org-2026 \
--enrollment-tokens-file /etc/aigo/enrollment-tokens.json \
--no-socket
The default bind address is 127.0.0.1 and the default port is 8444. --no-socket is optional; it keeps this TCP-only deployment from also opening the owner-only Unix socket.
Check whether an administrator account is needed:
Create the first administrator locally. The command below keeps the password out of the process argument list and shell history, and stores the returned session cookie in an owner-only file:
umask 077
read -r -s -p "Admin password: " AIGO_ADMIN_PASSWORD
printf '\n'
printf '%s' "$AIGO_ADMIN_PASSWORD" \
| jq -Rs '{username: "admin", password: .}' \
| curl -fsS \
-c admin.cookies \
-H 'Content-Type: application/json' \
--data-binary @- \
http://127.0.0.1:8444/api/v1/setup/register-admin
unset AIGO_ADMIN_PASSWORD
A loopback listener does not require a setup token because the endpoint is reachable only from the host. Do not expose the reverse proxy before completing this step. When the server is bound to a non-loopback address and no administrator exists, it instead prints a generated st-... setup token once to stderr. Supply that value as setupToken in the registration body. A restart before registration mints a different token.
The registration response creates an administrator session. Use that session to create a narrow API key for monitoring and operator probes:
curl -fsS \
-b admin.cookies \
-H 'Content-Type: application/json' \
--data-binary '{
"name": "policy-monitor",
"description": "Policy version monitoring",
"scopes": ["enterprise_read"],
"rateLimit": 0,
"createdBy": "admin"
}' \
http://127.0.0.1:8444/admin/keys
The response returns the new secret in data.secret once. Store it in a secrets manager. Later list and read operations return only masked key material. Administrator sessions are in memory and are lost on restart; the administrator account and API keys persist in the data directory. Sign in again through POST /api/v1/auth/login when a new session is needed.
HTTP surface¶
The server binds to --host (default 127.0.0.1, env AIGO_POLICY_HOST) and --port (default 8444, env AIGO_POLICY_PORT). The default port differs from the headless aigo-server default (8001) so a policy server and an inference server can share one box. Authentication cannot be disabled: the server overrides auth.require_api_key=false, because the policy and audit routes rely on device-signature verification before the enterprise_read scope check runs.
The complete route families mounted by this product are:
| Family | Routes | Auth | Purpose |
|---|---|---|---|
| Device enrollment | POST /api/v1/enroll | One-time enrollment token in the request body | Validates and consumes the token, then returns trust anchors and the poll interval. |
| Policy | GET /api/v1/policy, GET /api/v1/policy/version, POST /api/v1/audit | enterprise_read; enrolled devices authenticate each request with their device signature | Serves the signed policy and version metadata, and accepts signed device audit batches. |
| First-run and sign-in | GET /api/v1/setup/status, POST /api/v1/setup/register-admin, /api/v1/auth/{validate,scopes,session,login,logout} | Public bootstrap routes; credentials are checked by each handler | Creates the first administrator, signs operators in and out, and inspects credentials and scopes. |
| Account | GET /api/v1/auth/me, POST /api/v1/auth/password | Authenticated username/password session | Reads the current account and changes its password. |
| API-key administration | /admin/keys, /admin/keys/audit, and the per-key read, update, delete, rotate, enable, disable, and audit routes | keys_read or keys_write | Issues and manages operator API keys. |
Device-signature auth grants the narrow enterprise_read scope to enrolled devices. API-key and session auth remain available for operator and admin flows. Client audit reporting is covered in Audit and Compliance.
No model, inference, router, agent, plugin, memory, file, metrics, or peer route is mounted. Such requests return 404; use aigo-server for the inference product.
Run as a service (systemd)¶
Run the server under a service supervisor so it restarts on failure and starts on boot. Create a dedicated account, install the inputs with permissions the runtime actually accepts, and create the persistent data directory before loading the unit:
sudo useradd --system --home-dir /var/lib/aigo-policy-server \
--shell /usr/sbin/nologin aigo
sudo install -d -o root -g root -m 0755 /etc/aigo
sudo install -d -o aigo -g aigo -m 0700 /var/lib/aigo-policy-server
sudo install -o aigo -g aigo -m 0600 \
policy-signing.pk8.b64 /etc/aigo/policy-signing.pk8
sudo install -o aigo -g aigo -m 0600 \
enrollment-tokens.json /etc/aigo/enrollment-tokens.json
sudo install -o root -g root -m 0644 \
policy.json /etc/aigo/policy.json
If the aigo account already exists, omit useradd. Both secret files must be owned by the service account and mode 0600. A mode 0600 file owned by root is not readable by User=aigo, while a group-readable mode such as 0640 is deliberately rejected by the server.
The unit below binds only to loopback for a same-host TLS reverse proxy and disables the unused Unix socket. It gives the process read-only access to /etc/aigo and write access only to its persistent state directory:
[Unit]
Description=Backend.AI GO Policy Server
Documentation=https://github.com/lablup/backend.ai-go
After=network-online.target
Wants=network-online.target
RequiresMountsFor=/var/lib/aigo-policy-server
[Service]
Type=simple
User=aigo
Group=aigo
UMask=0077
ExecStart=/usr/local/bin/aigo-policy-server \
--host 127.0.0.1 \
--port 8444 \
--data-dir /var/lib/aigo-policy-server \
--signing-key-file /etc/aigo/policy-signing.pk8 \
--policy-file /etc/aigo/policy.json \
--anchor-id org-2026 \
--enrollment-tokens-file /etc/aigo/enrollment-tokens.json \
--no-socket
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_INET AF_INET6
RestrictNamespaces=true
LockPersonality=true
RestrictRealtime=true
RestrictSUIDSGID=true
ReadOnlyPaths=/etc/aigo
ReadWritePaths=/var/lib/aigo-policy-server
CapabilityBoundingSet=
AmbientCapabilities=
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
Save it as /etc/systemd/system/aigo-policy-server.service, then start it and complete the local administrator bootstrap before enabling the reverse proxy:
sudo systemctl daemon-reload
sudo systemctl enable --now aigo-policy-server
sudo systemctl status aigo-policy-server
journalctl -u aigo-policy-server -f
The data directory is not disposable. It contains the administrator database, access keys, consumed-token digests, and enrolled-device public keys. Do not place it under /tmp, and do not omit --data-dir while using ProtectHome=true; the fallback path is under the service account's home and is hidden by that sandbox setting.
The process handles SIGTERM for graceful shutdown but does not implement configuration reload. Use systemctl restart aigo-policy-server after a validated policy or token-document change. Do not add an ExecReload that sends SIGHUP.
Run in a container¶
The project does not publish an official policy-server container image. The supported release artifact is the bare Linux binary, so an operator who needs a container must build and maintain a private image from the version-matched binary. A minimal base image needs CA certificates and a health-probe client, and it must run the server as a non-root user with a persistent writable data volume:
FROM ubuntu:24.04
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl \
&& rm -rf /var/lib/apt/lists/* \
&& useradd --uid 10001 --user-group --no-create-home \
--home-dir /var/lib/aigo-policy-server \
--shell /usr/sbin/nologin aigo \
&& install -d -o aigo -g aigo -m 0700 /var/lib/aigo-policy-server
COPY --chown=aigo:aigo --chmod=0755 \
aigo-policy-server /usr/local/bin/aigo-policy-server
USER 10001:10001
ENTRYPOINT ["/usr/local/bin/aigo-policy-server", "--host", "0.0.0.0", "--port", "8444", "--data-dir", "/var/lib/aigo-policy-server", "--no-socket"]
Mount the signing key, token document, and policy read-only, then pass their container paths through the corresponding flags. The key and token mounts must appear inside the container as owner 10001 and mode 0600; common secret mounts that are group- or world-readable are rejected. Mount /var/lib/aigo-policy-server from a persistent volume, keep the root filesystem read-only where practical, and publish port 8444 only to the TLS terminator.
After bootstrapping an operator key with enterprise_read, store it in a container secret such as /run/secrets/policy-health-key and add a health check against the cheap version route:
HEALTHCHECK --interval=30s --timeout=5s \
CMD-SHELL curl -fsS http://localhost:8444/api/v1/policy/version \
-H "Authorization: Bearer $(cat /run/secrets/policy-health-key)" \
|| exit 1
Inspect the resolved health state with docker inspect:
Verify the deployment¶
After starting the server, confirm it serves a policy and that the version route responds. The version route is the cheapest check and is what clients poll:
curl -fsS https://policy.acme.example/api/v1/policy/version \
-H "Authorization: Bearer <operator-api-key>"
A healthy response carries the policy id (the policyId from your document) and its issue date. Clients compare this id against their cached id and only download the full policy when it changes.
For an update, the complete operator sequence is:
- Edit the authored or pre-signed policy and assign a new
policyIdandissuedAt. - Run the full startup command with
--dry-run. - Install the new file atomically at the configured path.
- Restart the service with
systemctl restart aigo-policy-server. - Read
/api/v1/policy/versionand confirm the new id before waiting one poll interval for client rollout.
The same restart rule applies after changing enrollment-token inputs. There is no SIGHUP or live file reload.
Command reference¶
| Flag | Env | Purpose |
|---|---|---|
--signing-key-file | AIGO_POLICY_SIGNING_KEY_FILE | PKCS#8 Ed25519 signing key (raw or base64). |
--anchor-id | AIGO_POLICY_ANCHOR_ID | Trust-anchor id recorded in anchorId and provisioned into clients. |
--policy-file | AIGO_POLICY_FILE | Authored EnterprisePolicy JSON, signed at startup. |
--signed-policy-file | AIGO_POLICY_SIGNED_FILE | Pre-signed SignedPolicy JSON, served verbatim. |
AIGO_POLICY_SIGNING_KEY | Base64 PKCS#8 Ed25519 signing key supplied directly through the environment. Prefer a secret manager or owner-only file. | |
--enrollment-tokens-file | AIGO_POLICY_ENROLLMENT_TOKENS_FILE | JSON map of token to org (null for no org). Must be mode 0600. |
--enrollment-tokens-stdin | Read the token-to-org JSON document from standard input. | |
AIGO_POLICY_ENROLLMENT_TOKENS | Supply the token-to-org JSON document directly through the environment. | |
--poll-interval-secs | AIGO_POLICY_POLL_INTERVAL_SECS | Poll interval returned to enrolling devices (default 900). |
--host / -H | AIGO_POLICY_HOST | Numeric bind address (default 127.0.0.1). |
--port / -p | AIGO_POLICY_PORT | Bind port (default 8444). |
--data-dir / -D | AIGO_POLICY_DATA_DIR | Persistent data directory for accounts, access keys, and the device registry. |
--config / -c | AIGO_POLICY_CONFIG | TOML config file (reuses the aigo-server config shape). |
--print-anchor | Print the public trust anchor and exit. | |
--generate-signing-key | Print a fresh base64 PKCS#8 Ed25519 key and exit. | |
--no-socket | Disable the Unix domain socket listener (TCP only). | |
--dry-run | Validate config, policy, and key, then exit. | |
--verbose / -v | Increase logging verbosity (-v, -vv). | |
--quiet / -q | Suppress non-error output. |
Next steps¶
- Choose how clients receive the policy in Deployment Models.
- Onboard devices in Device Enrollment and Trust Anchors.
- Follow the end-to-end setup and key-rotation procedures in the Administrator Guide.