Requirements
- Docker for the evaluation image and Compose. Air-gapped Compose installs need Docker's containerd image store.
- For an external database: Guard is tested on PostgreSQL 17. The pinned
postgresimage must not be older than your server, because backups use itspg_dump. - For Kubernetes: an external PostgreSQL, an Ingress controller that terminates TLS and redirects HTTP, and a TLS Secret or cert-manager.
- A publicly trusted certificate (or one from a CA your endpoints trust) before devices can enroll. The
guardCLI trusts public WebPKI roots only.
Try it
docker run -d --name guard -p 8443:8443 \
-v guard-data:/var/lib/runeward-guard \
ghcr.io/runeeward/runeward-guard:<release>
docker logs -f guard
The log prints the Console address (https://localhost:8443/console/), the organization name and the first administrator key, once. Store the key, sign in, and continue in the Console. The key stays in docker logs until you remove the container, so create your own administrator account and revoke the first key if that matters to you.
Warning: This is an evaluation install: one node, no redundancy and no backups. Don't enroll production endpoints into it.
| Variable | Default | Meaning |
|---|---|---|
GUARD_PUBLIC_URL |
https://localhost:8443 |
The origin users open. It must match the browser's address exactly, port included. |
GUARD_TLS |
internal |
internal uses a local CA kept in the volume. acme gets a public certificate (publish -p 443:8443 -p 80:8080 and set GUARD_ACME_EMAIL). files uses tls.crt and tls.key mounted at /etc/guard-try/tls/, readable by uid 10002. |
GUARD_ORGANIZATION |
Evaluation |
The first organization's name. |
The local CA is for trying the Console only. To enroll endpoints, use acme or files.
docker exec guard guard-try status # readiness and versions
docker exec guard guard-try dump > guard.dump # database dump
docker rm -f guard && docker volume rm guard-data # start over
To move to production, take a dump, set up a Compose install, and restore with restore.sh --dump guard.dump --confirm. To keep existing policy signatures, sealed credentials and the remote response key working, first copy every file in /var/lib/runeward-guard/secrets/ except postgres_password and database_url over the production install's, keeping owner uid 10001 and mode 0400. Otherwise devices must pin the new keys.
Production with Docker Compose
Production Compose uses the hosted service's own deployment and scripts: images pinned by digest, keys and passwords as root-only files, host hardening, encrypted off-server backups, restore drills, upgrades with automatic rollback, and monitoring with alerts. The one choice is where PostgreSQL runs.
Bundled PostgreSQL (GUARD_DATABASE=bundled, the default) runs the pinned postgres image on an internal network with no published port. generate-secrets.sh creates its password, database_migration_url for the owner (guard) and database_url for the application role (guard_app). deploy.sh creates that role and its grants on every deploy, upgrade and restore, and moves an older server to it on its next upgrade (see Database roles).
Your own PostgreSQL (GUARD_DATABASE=external):
-
Run
deploy/scripts/install.sh, then setGUARD_DATABASE=externalin/etc/runeward-guard/guard.env. -
Create a database and an owning role (
CREATE ROLE guard LOGIN PASSWORD '...'; CREATE DATABASE guard OWNER guard;), and the application role, which owns nothing (CREATE ROLE guard_app LOGIN PASSWORD '...' NOSUPERUSER NOBYPASSRLS NOCREATEDB NOCREATEROLE;). Use passwords without URL-reserved characters, such asopenssl rand -hex 24. -
Store both URLs, then generate the other secrets. URLs are read from stdin, never an argument:
printf 'postgres://guard_app:%[email protected]:5432/guard?sslmode=verify-full\n' "$APP_PASSWORD" \ | sudo /opt/runeward-guard/scripts/generate-secrets.sh --set database_url printf 'postgres://guard:%[email protected]:5432/guard?sslmode=verify-full\n' "$OWNER_PASSWORD" \ | sudo /opt/runeward-guard/scripts/generate-secrets.sh --set database_migration_url sudo /opt/runeward-guard/scripts/generate-secrets.shWithout
database_url,generate-secrets.shrefuses rather than use the bundled database. Withoutdatabase_migration_url, Guard Cloud uses one role for everything, and row-level security may not hold.deploy.shgrantsguard_appits privileges as the owner. -
Place
images.envand rundeploy.sh. Thepostgresservice isn't started.
Kubernetes
The chart deploys Guard Cloud and the edge behind an Ingress. PostgreSQL is external, and keys come from a Secret you create: the chart never generates or stores a key. Every container runs as non-root with a read-only root filesystem, all capabilities dropped (the edge keeps NET_BIND_SERVICE) and CPU and memory limits.
-
Create the database and role as for Compose.
-
Create the Secret.
database_urlis required. Leave out a key to keep its feature off:kubectl create namespace guard { printf 'database_url=postgres://guard:%[email protected]:5432/guard?sslmode=verify-full\n' "$PASSWORD" for key in policy_signing_key command_signing_key identity_credential_key \ identity_snapshot_key connector_credential_key sso_request_key; do printf '%s=%s\n' "$key" "$(openssl rand -hex 32)" done } | kubectl -n guard create secret generic guard-secrets --from-env-file=/dev/stdin -
Install with images pinned by digest and a TLS Secret for your host, then create the first organization:
helm install guard helm-charts/guard --namespace guard \ --set host=guard.example.com \ --set secrets.existingSecret=guard-secrets \ --set ingress.tlsSecretName=guard-tls \ --set image.cloud.digest=sha256:... --set image.edge.digest=sha256:... helm test guard --namespace guard kubectl -n guard exec deploy/guard-cloud -c guard-cloud -- \ guard-cloud bootstrap --organization "Your organization"
sso_request_key is required once Guard Cloud runs more than one replica. Back up the keys offline.
The Ingress must terminate TLS and redirect plain HTTP. Guard Cloud refuses Console requests that didn't arrive over HTTPS.
To have the chart request certificates instead, install cert-manager with a ready Issuer or ClusterIssuer, drop --set ingress.tlsSecretName=guard-tls, and add -f tls-values.yaml:
ingress:
certManager:
enabled: true
issuerRef:
name: existing-production-issuer
kind: ClusterIssuer
A successful helm --wait does not prove the certificate was issued or will renew. Check it with kubectl -n guard wait --for=condition=Ready certificate/guard-console-tls --timeout=180s, and monitor expiry.
Note: Per-device mTLS is not wired into the chart. Chart publication, a bundled PostgreSQL for Kubernetes, and full qualification of cluster installs, certificate renewal, upgrades and air-gapped installs are still open.
Air-gapped install
On a machine with registry access, pack a release's images into one bundle with checksums:
deploy/scripts/airgap-bundle.sh --images images.env --output guard-bundle
Carry the guard-bundle directory across, then load it.
# Compose, after install.sh
sudo deploy/scripts/airgap-load.sh --bundle guard-bundle
sudo install -m 0600 guard-bundle/images.env /etc/runeward-guard/images.env
sudo /opt/runeward-guard/scripts/deploy.sh
# Kubernetes, on every node
sudo deploy/scripts/airgap-k8s-import.sh --bundle guard-bundle # containerd
sudo deploy/scripts/airgap-k8s-import.sh --bundle guard-bundle --ctr "k3s ctr" # k3s
On Kubernetes, install the chart with the digests from images.env and pullPolicy: Never or IfNotPresent. The evaluation image loads with docker load -i guard-images.tar and needs no network at all.
For policy signing with no network path to the server, use local admin signing (see Optional keys and settings below).
Database roles
For row-level security to hold, Guard Cloud connects with two roles:
| Role | Owns | Setting |
|---|---|---|
Owner (guard) |
The schema. Runs migrations. | DATABASE_MIGRATION_URL (or DATABASE_MIGRATION_URL_FILE) |
Application (guard_app) |
Nothing. Not a superuser, no BYPASSRLS. |
DATABASE_URL |
Docker Compose sets this up: deploy.sh creates guard_app in the bundled database, and grants it in yours. Otherwise, create the application role once, as the owner or a superuser, in Guard's database:
CREATE ROLE guard_app LOGIN PASSWORD '...' NOSUPERUSER NOBYPASSRLS NOCREATEDB NOCREATEROLE;
GRANT CONNECT ON DATABASE guard TO guard_app;
GRANT USAGE ON SCHEMA public TO guard_app;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO guard_app;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO guard_app;
-- Tables and sequences that later migrations create:
ALTER DEFAULT PRIVILEGES FOR ROLE guard IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO guard_app;
ALTER DEFAULT PRIVILEGES FOR ROLE guard IN SCHEMA public
GRANT USAGE, SELECT ON SEQUENCES TO guard_app;
On PostgreSQL 14 or older, also run REVOKE CREATE ON SCHEMA public FROM PUBLIC. Then restart Guard Cloud. With Helm, put the owner's URL in the Secret's optional database_migration_url key and the application role's in database_url.
With only DATABASE_URL set, one role does both, and a superuser or BYPASSRLS role skips the policies. Guard Cloud logs which case applies at startup.
First organization
- Evaluation image: created on first start. Its administrator key is printed once.
- Compose and Kubernetes: run
guard-cloud bootstrap --organization "Your organization"in the Guard Cloud container. It prints the first administrator key once.
Sign in to the Console with that key, then:
-
Create an enrollment token. It is single-use and valid for 24 hours by default, at most 30 days:
curl -X POST -H "Authorization: Bearer $ADMIN_KEY" https://guard.example.com/v1/admin/enrollment-tokens \ -d '{"expires_in_hours": 4}' -
Enroll each device and sync it on a schedule:
guard cloud enroll --server https://guard.example.com --token rge_… --catalog-key ./catalog.pub guard cloud sync
--catalog-key pins the keys catalogs must be signed with. Without it, sync doesn't install catalogs. The Linux packages include a systemd timer that runs sync.
To rename the organization, use PATCH /v1/admin/organization with {"name": "New name"}, or guard-cloud rename-organization --organization-id UUID --name "New name". The name is a label. Devices, keys and sessions bind the organization ID, so they keep working.
Optional keys and settings
Each optional feature needs its own key: 32 random bytes in hex (openssl rand -hex 32). Leave a key out to keep its feature off. Every GUARD_*_KEY and DATABASE_URL can be read from a file named by the same variable with a _FILE suffix. The file must be owner-only (mode 0400 or 0600).
| Variable | Turns on |
|---|---|
GUARD_POLICY_SIGNING_KEY |
Server-side signing of Console-managed policy. Publishing needs two people. |
GUARD_COMMAND_SIGNING_KEY |
Remote response commands. Pin its public key on each device with guard cloud command-key --key FILE. |
GUARD_IDENTITY_CREDENTIAL_KEY |
Okta and Microsoft Entra ID connections. |
GUARD_IDENTITY_SNAPSHOT_KEY |
Identity-aware rules. |
GUARD_MCP_REFERENCE_KEY |
MCP registry discovery. Guard Cloud then reads the public registry every 6 hours. |
GUARD_SSO_REQUEST_KEY |
Protects single sign-on requests. Required once more than one replica runs. |
Two more settings matter for every install. GUARD_CONSOLE_ORIGIN is the exact HTTPS origin users open for the Console; browser sign-in stays off while it is unset. GUARD_PROCESSING_REGION is the region slug where telemetry is processed and stored, such as eu or us-east-1; Guard Cloud logs a warning while it is unset.
Behind a reverse proxy or Ingress, list the proxy in GUARD_TRUSTED_PROXIES. The login and enrollment attempt limits take the client address from X-Forwarded-For only when the request comes from a trusted proxy.
Policy can also be signed with an HSM (GUARD_POLICY_PKCS11_*) or Vault or OpenBao Transit (GUARD_POLICY_VAULT_*). For security-sensitive and air-gapped installs, choose local admin signing: an administrator signs each publish on their own machine or hardware key, and the server holds no signing key.
Operational settings such as background intervals, retention and pool size have defaults. Change one with --settings or GUARD_SETTING_<NAME>. Guard Cloud refuses to start on an unknown name or an out-of-range value.
Upgrades and backups
On Compose, the scripts work with bundled and external PostgreSQL:
| Script | Does |
|---|---|
backup.sh |
Encrypted backup with an off-server copy |
restore-drill.sh |
Restores a backup to check it |
restore.sh |
Restores a backup into the live database |
upgrade.sh |
Takes a backup, upgrades, and rolls back automatically if the upgrade fails |
With an external database, keep your provider's own backups and point-in-time recovery too. Air-gapped upgrades load a new bundle, then run upgrade.sh --images guard-bundle/images.env.
On Kubernetes, Guard Cloud applies migrations when it starts. Back up the database before helm upgrade to a new release.
- Upgrade Guard Cloud before its devices. An older server refuses uploads with fields it doesn't know.
- Back up as the owner. Backups and restores read every organization's rows:
PGOPTIONS='-c guard.cross_tenant=backup' pg_dump --enable-row-security ..., and the same forpg_restore. The Compose scripts already do this. After restoring into an emptied schema, run the application role grants again. - Check health.
GET /healthzanswers while the process runs.GET /readyzanswers 200 only when the database answers and its migrations match the binary. Keep both on the private network.
To delete an organization and all its data, run guard-cloud delete-organization --organization-id UUID --confirm-name NAME. Add --dry-run to see the row counts first. Backups taken earlier keep its data until they expire.
What Guard Cloud does, its security model and its limits are on Guard Cloud.
Written from docs/guard-cloud.md, docs/self-hosting.md at commit fb79778d02f0. Rune Guard is in early access.