Skip to content

Outpost software updates

The Arbitex Outpost supports two update methods:

  1. Signed bundle update API (shipped in outpost-0034) — A built-in update manager that downloads, verifies, and stages update bundles via the outpost admin API. Bundles are Ed25519-signed tarballs. Recommended for connected deployments.

  2. Docker image update — Pull and recreate the outpost container with a new image tag. Used for major upgrades, Kubernetes deployments, and environments where bundle-based updates are not configured.

Source: outpost/services/software_update.py, outpost/admin/routes.py


Starting with outpost-0034, the outpost includes a SoftwareUpdateManager service that manages the full update lifecycle: version check → bundle download → signature verification → staging. Updates are applied by restarting the outpost process — there is no auto-apply.

VariableRequiredDescription
SOFTWARE_UPDATE_RELEASE_URLYesURL to the JSON release manifest. When set, the update manager is initialized.
SOFTWARE_UPDATE_SIGNING_KEYYesBase64url-encoded 32-byte Ed25519 public key used to verify bundle signatures. Fail-closed: updates are rejected if this key is absent or the signature does not match.
UPDATE_STAGE_DIRNoDirectory for staged bundle files (default: /tmp/outpost-update-stage).
OUTPOST_VERSIONNoCurrent running version, for version comparison in the manifest check.

If SOFTWARE_UPDATE_RELEASE_URL is not set, all update API endpoints return a 503 with "Software update not configured".

The update process has three explicit steps, each triggered by an admin API call:

IDLE → CHECKING → AVAILABLE → DOWNLOADING → STAGED
↓ (restart)
APPLIED

If any step fails, the status transitions to ERROR. Re-trigger from the beginning (/check first).

Terminal window
POST /admin/api/updates/check
Authorization: Basic admin:<OUTPOST_EMERGENCY_ADMIN_KEY>

Fetches the release manifest from SOFTWARE_UPDATE_RELEASE_URL. The manifest must include version, download_url, and signature_url. Compares the manifest version against OUTPOST_VERSION.

Response:

{
"status": "available",
"current_version": "0.33.0",
"latest_version": "0.34.0",
"release_notes": "outpost-0034: signed bundle update API, policy simulator improvements",
"download_progress": null,
"staged_at": null,
"staged_version": null,
"staged_path": null,
"signature_verified": null,
"error": null
}

If already at the latest version, status is "idle".

Terminal window
POST /admin/api/updates/download
Authorization: Basic admin:<OUTPOST_EMERGENCY_ADMIN_KEY>

Downloads the bundle tarball and its Ed25519 signature file concurrently from the URLs in the manifest. Verifies the signature using the SOFTWARE_UPDATE_SIGNING_KEY public key.

Signature verification:

  • Uses the cryptography library’s Ed25519PublicKey.verify().
  • Fail-closed: if SOFTWARE_UPDATE_SIGNING_KEY is missing, the key is invalid, or the signature does not match, the download is rejected and status is set to "error". The bundle is never staged with a failed or missing signature.

Response on success:

{
"status": "staged",
"current_version": "0.33.0",
"latest_version": "0.34.0",
"download_progress": 1.0,
"staged_at": 1741824000.0,
"staged_version": "0.34.0",
"staged_path": "/tmp/outpost-update-stage/outpost-0.34.0.tar.gz",
"signature_verified": true,
"error": null
}

Response on signature failure:

{
"status": "error",
"error": "Signature verification failed — bundle rejected (fail-closed)",
"signature_verified": null
}
Terminal window
GET /admin/api/updates/status
Authorization: Basic admin:<OUTPOST_EMERGENCY_ADMIN_KEY>

Returns the current update manager state. Use this to poll during a download or to confirm staging before applying.

FieldDescription
statusidle / checking / available / downloading / staged / error
current_versionRunning outpost version
latest_versionVersion from the last manifest fetch
release_notesRelease notes from the manifest
download_progressFloat 0.0–1.0 during download, 1.0 when staged
staged_atUNIX timestamp when the bundle was staged
staged_versionVersion of the staged bundle
staged_pathFilesystem path to the staged .tar.gz
signature_verifiedtrue if the staged bundle passed signature verification
errorError message if status == "error"

No auto-apply: once status == "staged" and signature_verified == true, apply the update by restarting the outpost process:

Terminal window
# Docker Compose
docker compose -f docker-compose.outpost.yml restart outpost
# Kubernetes
kubectl rollout restart deployment/arbitex-outpost -n arbitex

On startup, the outpost extracts the staged bundle from staged_path and loads the new binary. After restart, verify with GET /admin/api/updates/statuscurrent_version should match staged_version and status should be "idle".

Air-gap update procedure (manual bundle placement)

Section titled “Air-gap update procedure (manual bundle placement)”

In air-gap deployments (OUTPOST_AIRGAP=true) or networks where the outpost cannot reach the release manifest URL, place bundles manually:

  1. Obtain the bundle from your Arbitex account team. The bundle package contains:

    • outpost-{version}.tar.gz — the update tarball
    • outpost-{version}.tar.gz.sig — the Ed25519 signature file
  2. Copy the bundle to the stage directory:

    Terminal window
    # Default stage dir
    cp outpost-0.34.0.tar.gz /tmp/outpost-update-stage/
    cp outpost-0.34.0.tar.gz.sig /tmp/outpost-update-stage/
  3. Trigger download (the download step also performs signature verification even for manually placed bundles, as long as the files exist at the manifest URLs or in the stage directory):

    For pure air-gap with no manifest server, use the direct bundle injection endpoint if available, or restart the outpost with the UPDATE_STAGE_DIR environment pointing to the bundle directory. The update manager will pick up the pre-placed bundle on startup.

  4. Verify signature before restart:

    Terminal window
    # Manually verify using the same Ed25519 key
    # (requires cryptography package)
    python3 - <<'EOF'
    import base64
    from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
    key_b64 = "<SOFTWARE_UPDATE_SIGNING_KEY>"
    raw_key = base64.b64decode(key_b64 + "==")
    public_key = Ed25519PublicKey.from_public_bytes(raw_key)
    bundle = open("outpost-0.34.0.tar.gz", "rb").read()
    sig = open("outpost-0.34.0.tar.gz.sig", "rb").read()
    public_key.verify(sig, bundle)
    print("Signature valid")
    EOF
  5. Restart the outpost to apply the staged bundle.



The outpost sends a heartbeat to the Platform management plane every 120 seconds via mTLS. The heartbeat payload includes the current running version:

{
"version": "0.1.0",
"uptime": 3600,
"policy_version": "abc123",
...
}

The Platform heartbeat response includes a latest_version field:

{
"latest_version": "0.2.0"
}

When the running version differs from latest_version, the outpost logs a warning:

WARNING outpost.heartbeat: Outpost version outdated: running=0.1.0 latest=0.2.0 — update recommended

The outpost admin status API also exposes this signal:

Terminal window
GET /admin/api/status

Response:

{
"outpost_id": "op_01abc123",
"policy_version": "abc123",
"update_available": true,
"latest_version": "0.2.0",
"uptime_seconds": 7200,
...
}

update_available: true means the Platform has signaled that a newer version is available. The admin panel displays an update notification banner when this field is true.


Open the outpost admin panel at http://localhost:8301 (or your outpost admin URL). If an update is available, a notification banner appears at the top of the Status page:

Update available: Outpost 0.2.0 is available (running 0.1.0). See the update guide to apply.

Poll the status endpoint directly:

Terminal window
curl -s \
-u admin:${OUTPOST_EMERGENCY_ADMIN_KEY} \
http://localhost:8301/admin/api/status \
| jq '{update_available, latest_version, policy_version}'

Policy sync status and signature verification

Section titled “Policy sync status and signature verification”

The sync-status endpoint shows the current policy bundle signature state alongside version information:

Terminal window
curl -s \
-u admin:${OUTPOST_EMERGENCY_ADMIN_KEY} \
http://localhost:8301/admin/api/sync-status \
| jq '{bundle_version, latest_version, bundle_signature_valid, bundle_verified_at}'
FieldDescription
bundle_versionPolicy bundle version currently loaded
latest_versionLatest outpost software version reported by Platform
bundle_signature_validWhether the loaded policy bundle passed signature verification
bundle_verified_atUNIX timestamp of the most recent signature check

bundle_signature_valid: true confirms the policy bundle was verified against the Platform’s signing key. This is separate from the software update — it verifies the policy configuration, not the container image.


The outpost runs as Docker Compose services. A software update involves pulling the new image and recreating the containers.

Step 1 — Back up the current configuration

Terminal window
cp .env .env.bak

Step 2 — Pull the new image

Terminal window
docker compose -f docker-compose.outpost.yml pull

This pulls arbitex-outpost:latest (or the pinned tag in your compose file) without stopping the running containers.

Step 3 — Verify the image signature

Arbitex signs all release images with Docker Content Trust (DCT). Verify before applying:

Terminal window
# Enable content trust for this command
DOCKER_CONTENT_TRUST=1 docker pull arbitex/outpost:0.2.0

If the image has not been signed or the signature does not match, Docker will refuse to pull it and print an error. Do not proceed if signature verification fails — contact Arbitex support.

For air-gapped deployments, see Signature verification in air-gap mode below.

Step 4 — Apply the update with a rolling restart

Terminal window
docker compose -f docker-compose.outpost.yml up -d --force-recreate

This recreates the containers using the newly pulled image. Existing connections to the proxy are gracefully terminated; new connections are handled by the updated container.

Step 5 — Verify the update

Wait 30 seconds for the outpost to fully initialize, then confirm the running version:

Terminal window
curl -s \
-u admin:${OUTPOST_EMERGENCY_ADMIN_KEY} \
http://localhost:8301/admin/api/status \
| jq '{update_available, latest_version}'

Expected after a successful update:

{
"update_available": false,
"latest_version": "0.2.0"
}

Also check the health endpoint:

Terminal window
curl -sf http://localhost:8300/health && echo "healthy"

If you run the outpost on Kubernetes via the Arbitex Helm chart:

Step 1 — Update the image tag in values

values-prod.yaml
image:
tag: "0.2.0"

Step 2 — Apply the Helm upgrade

Terminal window
helm upgrade arbitex-outpost arbitex/outpost \
-f values-prod.yaml \
--namespace arbitex

Helm performs a rolling update by default — old pods are kept running while new pods start and pass readiness checks.

Step 3 — Verify

Terminal window
kubectl rollout status deployment/arbitex-outpost -n arbitex
kubectl exec -n arbitex deployment/arbitex-outpost -- \
curl -s -u admin:${OUTPOST_EMERGENCY_ADMIN_KEY} http://localhost:8301/admin/api/status \
| jq .latest_version

In air-gap deployments (OUTPOST_AIRGAP=true), the outpost does not connect to the Platform management plane and cannot receive heartbeat responses. Update detection and application must be done manually.

Contact Arbitex support or your account team to receive the air-gap update bundle for the target version. The bundle contains:

  • arbitex-outpost-{version}.tar — the Docker image archive
  • arbitex-outpost-{version}.sha256 — SHA-256 checksum file
  • arbitex-outpost-{version}.sig — Ed25519 image signature
  1. Import the Arbitex release signing public key (provided by your Arbitex account team):

    Terminal window
    # Import the public key
    gpg --import arbitex-release-signing.pub
  2. Verify the signature on the image archive:

    Terminal window
    gpg --verify arbitex-outpost-0.2.0.sig arbitex-outpost-0.2.0.tar

    A successful verification prints:

    gpg: Good signature from "Arbitex Release Signing Key <releases@arbitex.ai>"

    Do not proceed if the signature is invalid or the key cannot be verified.

  3. Verify the checksum:

    Terminal window
    sha256sum -c arbitex-outpost-0.2.0.sha256

    Expected output: arbitex-outpost-0.2.0.tar: OK

Terminal window
docker load < arbitex-outpost-0.2.0.tar

Updating the policy bundle (air-gap policy sideload)

Section titled “Updating the policy bundle (air-gap policy sideload)”

Air-gap outposts load the policy bundle from a local volume rather than syncing from the Platform. To update the policy bundle:

  1. Obtain the new policy_bundle.json from your Arbitex account team (delivered as a signed archive using the same GPG key).

  2. Verify the signature on the bundle archive.

  3. Place the new bundle at the configured path:

    Terminal window
    # Default path (override with AIRGAP_POLICY_PATH)
    cp policy_bundle.json /opt/arbitex/policies/policy_bundle.json
  4. Restart the outpost to load the new bundle:

    Terminal window
    docker compose -f docker-compose.outpost.yml restart
  5. Verify the bundle loaded:

    Terminal window
    curl -s \
    -u admin:${OUTPOST_EMERGENCY_ADMIN_KEY} \
    http://localhost:8301/admin/api/sync-status \
    | jq '{bundle_version, bundle_signature_valid}'
  1. Tag the loaded image to match the compose file’s image reference:

    Terminal window
    docker tag arbitex-outpost:0.2.0 arbitex-outpost:latest
  2. Recreate the containers:

    Terminal window
    docker compose -f docker-compose.outpost.yml up -d --force-recreate
  3. Verify as in the standard update steps above.


The outpost verifies policy bundle signatures using a trusted Platform public key embedded at build time. In connected mode, the Platform signs bundles and the outpost verifies on each sync. In air-gap mode, the same verification runs when a new bundle is loaded.

bundle_signature_valid in the sync-status response reflects the most recent verification result. If this field is false after loading a new bundle, the bundle was not signed with the expected key — do not use it. Contact Arbitex support.


If the updated outpost is unhealthy or causing errors, roll back by specifying the previous image tag.

Step 1 — Edit the compose file to pin the previous version

services:
outpost:
image: arbitex/outpost:0.1.0 # previous version

Step 2 — Pull and recreate

Terminal window
docker compose -f docker-compose.outpost.yml pull
docker compose -f docker-compose.outpost.yml up -d --force-recreate

Step 3 — Verify

Terminal window
curl -s \
-u admin:${OUTPOST_EMERGENCY_ADMIN_KEY} \
http://localhost:8301/admin/api/status \
| jq .update_available
# Expected: true (since the rolled-back version is older than latest)

After rollback, update_available will be true again — that is expected. Investigate the issue before re-applying the update.

Terminal window
kubectl rollout undo deployment/arbitex-outpost -n arbitex
kubectl rollout status deployment/arbitex-outpost -n arbitex

This rolls back to the previous ReplicaSet. Helm history is also available:

Terminal window
helm history arbitex-outpost -n arbitex
helm rollback arbitex-outpost <revision> -n arbitex

Before applying any update:

  • Read the release notes for the target version (available at https://docs.arbitex.ai/changelog/outpost).
  • Verify the image signature before loading.
  • Back up .env and any local configuration.
  • Test the update in a non-production outpost first if possible.
  • Confirm bundle_signature_valid: true after restart.
  • Confirm update_available: false after restart (connected mode) or check the version field directly (air-gap mode).
  • Run a test request through the proxy to confirm DLP and policy evaluation are functioning.