Deployment guide

OPAL

Overview

OPAL (Open Policy Administration Layer) keeps authorization policy in sync, in real time. You keep your policy in a git repository, and OPAL server watches that repository. The moment you commit a change, OPAL pushes it out to every Open Policy Agent in your fleet.

Applications ask Open Policy Agent for authorization decisions and always get an answer based on the current policy. There is no redeploy, no restart and no polling loop of your own. OPAL on its own is infrastructure rather than a finished system, so this image ships a complete single-node appliance: OPAL server, OPAL client and Open Policy Agent, installed, wired together and working from first boot.

What you get:

  • OPAL 0.9.6 (server and client) installed into an isolated Python virtualenv at /opt/opal/venv, run by an unprivileged opal system account.
  • Open Policy Agent 1.18.2 at /usr/local/bin/opa, run by an unprivileged opa system account and bound to loopback.
  • A working default policy repository seeded on the VM at /var/lib/opal/policy-repo.git, containing an example Rego v1 policy. You can repoint it at your own repository with a single configuration change.
  • nginx on TCP 443 with a per-VM TLS certificate and per-VM HTTP basic-auth credential, exposing only Open Policy Agent's decision API.
  • Every secret (master token, RS256 signing keypair, client JWT, basic-auth password and TLS certificate) generated uniquely on the first boot of every VM into a root-only file.
  • A shipped self-test at /usr/local/sbin/opal-selftest.sh that proves the whole round trip end to end.
  • 24/7 SecureAnyCloud support.

A note on the upstream project. OPAL is actively maintained but moves slowly, and we would rather you knew that up front. The repository is not archived, and recent commits are genuine engineering work by the Permit.io team. The cadence in 2026 has been roughly one to three commits per month, and there was a four-month period between late November 2025 and the end of March 2026 with no commits to the default branch at all. OPAL has also not yet reached a 1.0 release.

It is a sound, widely used project, with over five thousand GitHub stars. Even so, do not size your plans around rapid upstream fixes.

Prerequisites

  • An active Azure subscription, an SSH key pair, and a VNet plus subnet in the target region.
  • Standard_B2s (2 vCPU / 4 GiB RAM) is a sensible starting point.
  • NSG inbound rules: allow 22/tcp from your management network and 443/tcp from the applications that will ask this appliance for authorization decisions.
  • Port 80 is deliberately not used, and Open Policy Agent is deliberately not exposed. See the Security model section.

Step 1 - Deploy from the Azure Marketplace

Sign in to the Azure portal, choose Create a resource, search the Marketplace for OPAL by SecureAnyCloud, and select Create.

On Basics, pick your subscription, resource group, region and size. Under Administrator account, choose SSH public key and paste your key. Under Inbound port rules, allow SSH (22) and HTTPS (443). Then select Review + create and Create.

Step 2 - Deploy from the Azure CLI

# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:opal-ubuntu-24-04:default:latest
az vm create \
  --resource-group <your-rg> \
  --name opal \
  --image sac1647283583153:opal-ubuntu-24-04:default:latest \
  --size Standard_B2s \
  --admin-username azureuser \
  --ssh-key-values ~/.ssh/id_ed25519.pub \
  --vnet-name <your-vnet> --subnet <your-subnet> \
  --public-ip-sku Standard
az vm open-port --resource-group <your-rg> --name opal --port 443 --priority 1010

Step 3 - Connect to your VM

ssh azureuser@<vm-public-ip>

Step 4 - Confirm the appliance is running

Six units make up the appliance, and they start in a strict order on every boot:

  • opal-firstboot generates this VM's secrets and seeds its policy repository.
  • Open Policy Agent and OPAL server start.
  • opal-issue-client-token mints the client's JWT from the running server.
  • OPAL client connects and begins syncing.
  • nginx starts last.
for s in opal-firstboot opa opal-server opal-issue-client-token opal-client nginx; do printf '%-34s %s\n' "$s.service" "$(systemctl is-active $s.service)" done

Expected output:

opal-firstboot.service active opa.service active opal-server.service active opal-issue-client-token.service active opal-client.service active nginx.service active

Check what is listening. Only 22 and 443 are reachable from outside the VM. The entire control plane is bound to loopback:

ss -tln | tail -n +2 | awk '{print $4}' | sort -u
0.0.0.0:22 0.0.0.0:443 127.0.0.1:7000 127.0.0.1:7002 127.0.0.1:8181 127.0.0.53%lo:53 127.0.0.54:53 [::]:22 [::]:443

8181 is Open Policy Agent, 7002 is OPAL server and 7000 is OPAL client. All three are loopback only.

Step 5 - Retrieve this VM's credentials

Every secret is generated on the first boot of every VM and written to a root-only file. Two VMs launched from this image never share a credential.

sudo cat /root/opal-credentials.txt
# sac OPAL appliance - generated on FIRST BOOT by opal-firstboot.service.
# Every value below is unique to THIS VM. Store them somewhere safe.
#
# Appliance address (HTTPS, self-signed certificate): OPAL_APPLIANCE_URL=https://20.124.112.113/ OPA_DECISION_URL=https://20.124.112.113/v1/data/sac/authz/allow
# HTTP basic-auth credential for the exposed decision API on port 443: OPAL_BASIC_AUTH_USER=opal OPAL_BASIC_AUTH_PASSWORD=<set-at-first-boot>
# OPAL server master token (administrative - mints client tokens): OPAL_AUTH_MASTER_TOKEN=<set-at-first-boot>

The appliance ships with a self-signed TLS certificate, so the examples below pass curl -k. Replace it with your own certificate for production, as described in the Replacing the TLS certificate section.

Step 6 - Ask for an authorization decision

The seeded policy lives in package sac.authz, so its allow rule is exposed at /v1/data/sac/authz/allow. Send the facts of the request as input:

curl -sk -u opal:<OPAL_BASIC_AUTH_PASSWORD> \
  -X POST https://127.0.0.1/v1/data/sac/authz/allow \
  -H 'Content-Type: application/json' \
  -d '{"input":{"user":"alice","action":"read"}}'
{"result":true}

Now ask about a user whom the policy does not permit:

curl -sk -u opal:<OPAL_BASIC_AUTH_PASSWORD> \
  -X POST https://127.0.0.1/v1/data/sac/authz/allow \
  -H 'Content-Type: application/json' \
  -d '{"input":{"user":"bob","action":"read"}}'
{"result":false}

The policy denies by default, so anything it does not explicitly allow comes back false.

Step 7 - Look at the policy OPAL is serving

OPAL server tracks a git repository. The image seeds a working default repository on the VM, so the appliance is useful immediately:

grep -E '^OPAL_POLICY_REPO_URL=|^OPAL_POLICY_REPO_MAIN_BRANCH=|^OPAL_POLICY_REPO_POLLING_INTERVAL=' /etc/opal/server.env
OPAL_POLICY_REPO_URL=file:///var/lib/opal/policy-repo.git OPAL_POLICY_REPO_MAIN_BRANCH=master OPAL_POLICY_REPO_POLLING_INTERVAL=15

The policy itself:

sed -n '/^package/,$p' /var/lib/opal/policy-work/policies/example.rego
package sac.authz import rego.v1
# Deny by default. Every allow rule below must match explicitly. default allow := false
# alice may read. allow if { input.user == "alice" input.action == "read" }
# Maintainers may read and write. allow if { input.role == "maintainer" input.action in {"read", "write"} }

/var/lib/opal/policy-work is a working clone you can edit and commit in; /var/lib/opal/policy-repo.git is the bare repository OPAL server actually watches.

Step 8 - Watch a policy change propagate live

This is what OPAL is for. Change the policy in git, and the authorization decision changes with no restart and no redeploy. The commands below add a rule allowing bob to read, wait for OPAL to distribute it, then roll the change back:

cd /var/lib/opal/policy-work GIT="sudo -u opal -H git -c user.email=opal@sac.local -c user.name=sac" decision() { curl -sk -u opal:<OPAL_BASIC_AUTH_PASSWORD> -X POST https://127.0.0.1/v1/data/sac/authz/allow -H 'Content-Type: application/json' -d '{"input":{"user":"bob","action":"read"}}'; } echo "before: $(decision)" cat >> policies/example.rego <<'REGO' allow if { input.user == "bob" input.action == "read" } REGO $GIT commit --quiet -am "allow bob to read" $GIT push --quiet origin master for i in $(seq 1 20); do [ "$(decision)" = '{"result":true}' ] && { echo "after: $(decision) (propagated in ~$((i*2))s)"; break; } sleep 2 done $GIT revert --no-edit --quiet HEAD $GIT push --quiet origin master for i in $(seq 1 20); do [ "$(decision)" = '{"result":false}' ] && { echo "reverted: $(decision)"; break; } sleep 2 done
before: {"result":false} after: {"result":true} (propagated in ~18s) reverted: {"result":false}

The change reached Open Policy Agent within one polling interval. OPAL_POLICY_REPO_POLLING_INTERVAL is set to 15 seconds on this image. Lower it for faster propagation, or drive updates instantly with a git webhook instead of polling.

Step 9 - Point OPAL at your own policy repository

The seeded repository is a starting point. To serve your own policies, change one setting:

sudo sed -i 's|^OPAL_POLICY_REPO_URL=.*|OPAL_POLICY_REPO_URL=https://github.com/your-org/your-policy-repo.git|' /etc/opal/server.env
sudo systemctl restart opal-server.service

You will likely also want these settings in /etc/opal/server.env:

  • OPAL_POLICY_REPO_MAIN_BRANCH: the branch to track. It defaults to master. Set it to main for most GitHub repositories.
  • OPAL_POLICY_REPO_POLLING_INTERVAL: the number of seconds between polls. Set it to 0 to disable polling and drive updates by webhook instead.
  • OPAL_POLICY_REPO_SSH_KEY: the deploy key for a private repository.
  • OPAL_FILTER_FILE_EXTENSIONS: which files form the policy bundle. It defaults to .rego,.json. This is a comma-separated list, not JSON. A value like [".rego"] is parsed literally and silently matches nothing.

Your .rego files can live anywhere in the repository, and OPAL bundles them all. After restarting OPAL server, confirm that the new bundle arrived:

sudo journalctl -u opal-client.service --no-pager | grep -c 'Got policy bundle'

Step 10 - Run the built-in self-test

The image ships the same end-to-end test the build pipeline uses. The test performs these checks:

  • It loads a known policy and asserts the expected allow and deny decisions.
  • It commits a policy change and asserts that the decision genuinely changes, then reverts the change.
  • It runs the security negative controls.

Run it with:

sudo /usr/local/sbin/opal-selftest.sh
[1/3] policy distributed: alice/read=allow, bob/read=deny, alice/write=deny [2/3] LIVE UPDATE: a git commit flipped bob/read deny => allow [3/3] security: unauth=401, dev-secret=401, per-VM cred=200, PUT=403, /v1/policies=403, control plane loopback-only SELFTEST_OK

Security model

Open Policy Agent's API is unauthenticated by default, and its Data API can overwrite policy. An exposed Open Policy Agent is therefore a complete authorization bypass, because anyone who can reach it can rewrite the rules that decide who is allowed to do what. This image is built around that fact:

  • Open Policy Agent is bound to 127.0.0.1:8181 and is never reachable from outside the VM.
  • OPAL server (127.0.0.1:7002) and OPAL client (127.0.0.1:7000) are loopback only. OPAL's own control channels are additionally authenticated with a per-VM master token and an RS256 JWT signed by a per-VM 4096-bit key. Upstream ships the well-known development value THIS_IS_A_DEV_SECRET as the default for these, and this image overrides both.
  • nginx on 443 is the only externally reachable service. It requires HTTP basic auth over TLS and proxies a deliberately narrow slice of Open Policy Agent: GET and POST under /v1/data/ for decision queries, plus /health. Every other path returns 403, and /v1/policies is never proxied at all.
  • The exposed surface cannot change anything. PUT, PATCH and DELETE are rejected before they reach Open Policy Agent. Even a caller holding the basic-auth credential can only ask questions, not rewrite policy or data.

You can confirm all of this yourself:

D='{"input":{"user":"alice","action":"read"}}' c() { curl -sk -o /dev/null -w '%{http_code}' -m 15 "$@"; } printf '%-46s HTTP %s\n' 'no credentials' "$(c -X POST https://127.0.0.1/v1/data/sac/authz/allow -H 'Content-Type: application/json' -d "$D")" printf '%-46s HTTP %s\n' 'this VM credential' "$(c -u opal:<OPAL_BASIC_AUTH_PASSWORD> -X POST https://127.0.0.1/v1/data/sac/authz/allow -H 'Content-Type: application/json' -d "$D")" printf '%-46s HTTP %s\n' 'PUT (overwrite attempt)' "$(c -u opal:<OPAL_BASIC_AUTH_PASSWORD> -X PUT https://127.0.0.1/v1/data/sac/x -H 'Content-Type: application/json' -d '[1]')" printf '%-46s HTTP %s\n' 'GET /v1/policies' "$(c -u opal:<OPAL_BASIC_AUTH_PASSWORD> https://127.0.0.1/v1/policies)"
no credentials HTTP 401 this VM credential HTTP 200 PUT (overwrite attempt) HTTP 403 GET /v1/policies HTTP 403

To let applications on other hosts query this appliance, allow 443/tcp from just those hosts in the NSG and give them the basic-auth credential. Do not open 8181.

Replacing the TLS certificate

The per-VM certificate is self-signed, which is why the examples use curl -k. To use your own certificate:

sudo cp your-cert.crt /etc/opal/tls/server.crt
sudo cp your-cert.key /etc/opal/tls/server.key
sudo chown root:www-data /etc/opal/tls/server.key
sudo chmod 0640 /etc/opal/tls/server.key
sudo nginx -t && sudo systemctl reload nginx

Changing the basic-auth credential

sudo htpasswd -B /etc/opal/htpasswd opal
sudo systemctl reload nginx

Distributing data as well as policy

OPAL distributes data as well as policy. Data is the set of facts your policies reason over, such as role assignments or entitlements pulled from your own systems.

That channel is driven by OPAL data sources rather than by files in the policy repository. It is not configured on this image because it depends entirely on where your data lives. See the OPAL data sources documentation to wire it to a database, an API or a message bus.

Maintenance

The image is patched at build time and leaves Ubuntu's unattended-upgrades enabled, so OS security updates continue to apply on your VM. To update the appliance components, run:

# Open Policy Agent
sudo curl -fsSL -o /usr/local/bin/opa https://github.com/open-policy-agent/opa/releases/download/<tag>/opa_linux_amd64_static
sudo chmod 0755 /usr/local/bin/opa
sudo systemctl restart opa.service
# OPAL
sudo /opt/opal/venv/bin/pip install --upgrade opal-server opal-client
sudo systemctl restart opal-server.service opal-client.service

The exact dependency set this image was built with is recorded at /opt/opal/requirements.lock.txt.

To read the logs for any component, run:

sudo journalctl -u opal-server.service --no-pager | tail -20

Licensing

OPAL and Open Policy Agent are both licensed under the Apache License 2.0. This image bundles no source-available or non-commercial components. The full resolved dependency set was checked at the artifact level before the image was built.

Support

Every SecureAnyCloud deployment is backed by 24/7 SecureAnyCloud support. For questions about running OPAL on Azure, contact sales@secureanycloud.com.

Deploy on Azure

Launch OPAL on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure