CloudPanorama Request evaluation

Install

Pull the signed image, verify it, and run it: a running product with a fabricated estate, then the same stack for a real one.

Access, today. The product is pre-release and self-hosted. The quickstart below pulls the published, signed image and, once that image is public, needs no access to anything; building from source needs access to the repository, which stays private until the first pilot. For a trial licence, ask through the contact on the site and say which estate you would evaluate it against. Nothing on this page phones home and nothing is collected until you connect an account.

Quickstart

The platform is one published, signed image. You need Docker with Compose v2, openssl for the passwords, and cosign 3 or later to check the signature. Nothing is built on your machine, and no repository access or registry login is needed: the image is public.

A release is a version and the digest it names; the digest is in the release record. This page installs 0.1.2, released on 2026-10-24, the first release whose image carries every deployment file these pages name:

IMAGE=ghcr.io/cloudpanorama/panorama/panorama:0.1.2@sha256:f487a34623502b37bbed14b3a2dfbde9f3d773e3c5715a94a99e2fd294c446c2   # the release's version and digest
docker pull "$IMAGE"
cosign verify "$IMAGE" \
  --certificate-identity https://github.com/cloudpanorama/panorama/.github/workflows/platform-release.yml@refs/tags/platform/v0.1.2 \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

cosign verify accepts the digest only if it was signed by the repository's release workflow, on the tag of that version, through GitHub's own identity provider, and the signature is in the public transparency log. A digest that was not, or a different identity, fails here — stop and ask. Use cosign 3 or later: the release is signed in cosign 3's bundle format, and an older cosign does not read it and reports "no signatures found" for a correctly signed image.

The image runs natively on linux/amd64 and linux/arm64 (an Apple-silicon Mac, an ARM VM): the digest is a multi-platform index, docker pull takes your host's platform from it, and the release signs the index and each platform's image, so cosign verify passes on either digest (docker buildx imagetools inspect "$IMAGE" lists them). Each platform's image carries its own attested CycloneDX SBOM, attested to the index as well: cosign verify-attestation --type cyclonedx with the same two flags returns both.

The deployment files come out of the image you have just verified, and the passwords are generated on the spot:

mkdir -p panorama/deploy && cd panorama
id=$(docker create "$IMAGE") && docker cp "$id:/deploy/." deploy/ && docker rm "$id"
( umask 077; {
  echo "PANORAMA_IMAGE=$IMAGE"
  for v in POSTGRES_PASSWORD PANORAMA_APP_PASSWORD PANORAMA_CONTROL_PASSWORD; do
    echo "$v=$(openssl rand -hex 24)"
  done
  echo "SEED_DEMO=true"                # the demo estate; leave it out for a real stack
} > deploy/.env )
sh deploy/compose-worker-token.sh deploy/.env deploy/docker-compose.yml   # the workers' ingest credential, into deploy/.env
docker compose -f deploy/docker-compose.yml up -d     # Postgres + every role, migrated, seeded
docker compose -f deploy/docker-compose.yml logs seed # the demo admin's password, printed once

The copied deploy/ is everything the rest of these pages use, in one layout, signed with the image: the stack itself (deploy/docker-compose.yml, deploy/bootstrap-roles.sql, deploy/compose-worker-token.sh, deploy/.env.example); the TLS edge's proxy example, deploy/proxy/nginx.conf.example; the reader templates — deploy/cloudformation/reader-role.yaml and deploy/cloudformation/cost-report-role.yaml for AWS, deploy/gcp/reader-role.yaml for Google Cloud, deploy/azure/reader.bicep with its role files for Azure; deploy/cloudformation/platform-identity.yaml, the platform's own AWS identity, with its guide deploy/cloudformation/platform-identity.md; the Kubernetes manifests, deploy/k8s/ and deploy/overlays/example/; and, for the provisioning agent, its stack deploy/cloudformation/agent-runtime.yaml and the runner templates deploy/cloudformation/library-runner-roles.yaml, deploy/gcp/library-runners/ and deploy/azure/library-runners/. Each later step names its file as deploy/<path>, so run it from this directory. The image carries all of them from platform 0.1.2; 0.1.1's carries only the first three files named here.

Then open http://127.0.0.1:18080 and sign in as admin@demo.invalid with that password. The public status page — no login — is http://127.0.0.1:18080/status/demo.

The demo tenant contains three fabricated accounts, 200 EC2 instances, Kubernetes nodes and pods, and thirty days of operational history. It also includes four tag-defined applications and teams, sixty days of spend, budgets, anomalies, recommendations, Kubernetes cost allocation, and provisioning templates, requests, approvals, simulated logs and drift. Account IDs start at 000000000001 and example names use demo-. The seed makes no AWS calls, executes no provisioning and sends no notifications. The example agent is revoked and the webhook is disabled. Re-running the seed upgrades an inventory-only demo and preserves its login.

No credential is shipped and there is no default password anywhere, the demo admin's included. Ingest is authenticated, so the install issues the worker fleet's credential before the roles start (the compose-worker-token.sh step above: panorama admin create-worker-token, run from the image against the migrated database, written to deploy/.env as WORKER_TOKEN and printed by nobody). The script refuses a deploy/.env that is not mode 0600 rather than append a credential to it, and does nothing when a credential is already chosen. The workers reach serve inside the compose network, and the proxy example (deploy/proxy/nginx.conf.example) refuses the ingest routes at the edge. Behind that proxy, set CLIENT_ADDRESS_SOURCE=proxy and TRUSTED_PROXIES in deploy/.env (deploy/.env.example says which address), or the login throttle counts every client as the proxy. /readyz answers 200 about ten seconds after up. The host ports are 18080 and 55432 rather than 8080 and 5432 because the other pillars of this workspace hold the obvious ones; override with SERVE_PORT and POSTGRES_PORT.

docker compose -f deploy/docker-compose.yml logs -f    # follow every role
docker compose -f deploy/docker-compose.yml down       # stop; --volumes also deletes the database

An upgrade is the same three steps for the new release: set PANORAMA_IMAGE in deploy/.env to its version and digest, docker pull and cosign verify it, and docker compose -f deploy/docker-compose.yml up -d — the migration runs before the roles start. Copy the deployment files out of the new image again when its release notes say they changed.

Without SEED_DEMO=true the same command brings up an empty, real stack. Its first tenant is created with panorama admin bootstrap, which prints the administrator's generated password once:

docker compose -f deploy/docker-compose.yml run --rm serve admin bootstrap \
    --tenant-slug acme --tenant-name 'Acme' --email you@example.com

A real deployment sets its settings before the first up (the clean-install drill). Leave SEED_DEMO=true out of deploy/.env and add, before the worker-token step and up:

cat >> deploy/.env <<'EOF'
ENTITLEMENTS_UNRESTRICTED=false     # compose defaults it to true; a licence refuses it
COOKIE_SECURE=true                  # TLS terminates in front of serve
PORTAL_URL=https://panorama.example.com
OPERATOR_CONTACT=ops@example.com    # the Help menu's contact; the pilot gate requires one
EOF

A source build also sets LICENSE_PUBLIC_KEY there before make compose-up (it is baked into the image at build); make compose-up writes deploy/.env only when it is absent, so run make deploy/.env first to generate the passwords, append the settings, then make compose-up.

Compose publishes serve on 127.0.0.1:18080 only; put a TLS terminator on the same host in front of it — deploy/proxy/nginx.conf.example, with its upstream set to that port — and get its certificate however you get certificates (Let's Encrypt's certbot works). The example already refuses the ingest routes. For a proxy on the host, TRUSTED_PROXIES is the compose network's gateway, which exists only once the stack has started once:

docker network inspect panorama_default -f '{{range .IPAM.Config}}{{.Gateway}}{{end}}'

Once the stack is up, append CLIENT_ADDRESS_SOURCE=proxy and TRUSTED_PROXIES=<that gateway> to deploy/.env and run docker compose -f deploy/docker-compose.yml up -d again. Set the two together: serve refuses proxy without TRUSTED_PROXIES, and with peer the login throttle counts every client as the proxy.

From there, people are added, reset and removed without SQL — the portal's Users page, or panorama admin create-user, reset-password, set-role, disable-user, enable-user and revoke-sessions (docs/runbook.md, "Onboarding and removing people").

Building from source instead

The alternative to the published image, for a checkout of the repository (which needs access to it). It builds the same image on the machine, so it needs Docker with BuildKit (on Ubuntu, the docker-buildx package), make, openssl, and about 6 GB of memory with swap — on 4 GB the Go build is killed (compile: signal: killed).

git clone https://github.com/cloudpanorama/panorama && cd panorama/observability
SEED_DEMO=true make compose-up      # builds panorama:local, then the same stack

The image is built once, by the migrate role; the other roles run the image it tags and never try to pull panorama:local from a registry.

make compose-up generates deploy/.env with random passwords and PANORAMA_IMAGE=panorama:local the first time it runs, and adds deploy/docker-compose.build.yml to build it; every other command on this page is unchanged. make compose-logs, make seed (RESET=1 replaces the demo) and make compose-down (WIPE=1 deletes the database volume) are its shortcuts. A source build accepts licences only when LICENSE_PUBLIC_KEY is set in deploy/.env before it; the published image carries the product's key.

Kubernetes

The same image runs each role as its own Deployment, with migrate as a Job before them, against a Postgres you provide — the compose shape, productionised. The manifests are deploy/k8s, in the deploy/ the quickstart copies out of the image (from platform 0.1.2), a kustomization whose image names no usable tag: you deploy a copy of deploy/overlays/example, beside it in deploy/overlays/, which pins the image by digest, and add the namespace and the components you use — eks-irsa for IRSA on EKS, external-secrets to fill the Secret from your secret store.

What it renders

  • configmap.yaml
  • migrate-job.yaml
  • serve.yaml
  • schedule.yaml
  • work.yaml
  • notify.yaml
  • cleanup.yaml
  • serviceaccounts.yaml
  • pdb.yaml
  • networkpolicy.yaml

The Secret

Named panorama-secrets, with exactly these keys — three database URLs for three database roles, and the worker fleet's credential:

  • CONTROL_DATABASE_URL
  • DATABASE_URL
  • MIGRATE_DATABASE_URL
  • WORKER_TOKEN

Apply

kubectl create secret generic panorama-secrets --from-literal=DATABASE_URL=... \
    --from-literal=CONTROL_DATABASE_URL=... --from-literal=MIGRATE_DATABASE_URL=... \
    --from-literal=WORKER_TOKEN=placeholder
deploy/k8s/deploy.sh <your overlay> <namespace>    # serve waits for a credential
kubectl exec deploy/panorama-work -- /panorama admin create-worker-token --name 'cluster workers' --raw
# put the token in the Secret's WORKER_TOKEN, then:
deploy/k8s/deploy.sh <your overlay> <namespace>

On a first install the worker token cannot exist yet: minting it needs the migrated schema, which deploy.sh creates, and serve refuses to start until a credential exists. So the first deploy names serve and exits 1 with every other role up; the token is minted from a running work pod (--raw prints it alone), and the second deploy, with the Secret's new version, rolls every role. deploy.sh deploys in two phases: it recreates and applies the migrate Job on its own, waits for it to complete — a failed migration stops there, with the roles untouched — and only then applies the roles and waits for every Deployment's rollout, naming, describing and logging any role that did not get there before it exits 1; one kubectl apply -k would roll new binaries out regardless of the schema and could not update a finished Job's immutable template on the next release. Each pod template carries a digest of the ConfigMaps and the Secrets' versions, so a release that only changes configuration, or only rotates the Secret, rolls the pods that read it — rotating a credential is running deploy.sh again.

Before the first apply, fill TRUSTED_PROXIES in the ConfigMap with your ingress controller's addresses — serve refuses to start while it is empty, because a proxy header trusted from anywhere would let any pod choose the login throttle's key — and run bootstrap-roles.sql once as the database owner. What is deliberately not in the manifests: an Ingress (TLS terminates at your edge), the database, autoscaling, and the hosted track's environments.

The provisioning agent

Provisioning runs in your account, never in ours: a small agent you install where your infrastructure credentials already are. Its own page covers install, configuration, verification and upgrade.

Then

Connect a real AWS account, install the licence you were issued, and keep the runbook to hand.

Generated from observability/README.md (972fb451057a), observability/deploy/k8s/kustomization.yaml (341999132dcf), observability/deploy/k8s/secret.example.yaml (d40c5939f819), observability/deploy/k8s/deploy.sh (10fd3770ea54) by www/tools/docs; the value in brackets is a hash of the source's content, so a page changes only when its source does.