VxCloud
Back to Blog
Securityvxcloud original

Zero Trust Configuration: The 90-Day Hardening Playbook

Twelve configuration steps in risk-reduction order, with the actual config files and the test that proves each control is live: phishing-resistant MFA, scoped keys, least-privilege policy, Vault secrets, default-deny egress, SPIFFE and STRICT mTLS, WireGuard, OPA/Rego in CI, hash-chained audit and Terraform gating.

vxcloud Security Engineering

vxcloud Security Engineering

@vxcloud

Zero Trust architecture, identity and access, secrets, network segmentation and audit — from the security engineering team at prodxcloud.

Aug 5, 2026/ 24 min read
Zero Trust configuration playbook — a terminal running hardening commands beside a padlock assembled from policy blocks

Zero Trust configuration playbook — a terminal running hardening commands beside a padlock assembled from policy blocks

Photo: vxcloud Security Engineering
356 48

You already accept that network-location trust is dead. Now you need to configure something on Monday morning. This is the implementation companion: twelve configuration steps in risk-reduction order, with the actual files, the actual commands, and — critically — the test that proves each control is live.

A control you have not tested is not a control. It is a comment.

The twelve-step Zero Trust hardening playbook laid out across a 90-day timeline, each step paired with the test that proves it

Before you touch a config file: measure your blast radius

Do not start with technology. Start with a number, because without it you cannot prove the programme worked and you will lose its budget in the next cost review.

Blast radius = the count of resources reachable by one compromised principal. Pick your most ordinary engineer. Assume their laptop is owned right now — because in the scenario you are defending against, it is.

# Day 0 baseline. Run it, write the number down, put it on a slide.

# 1. What can that identity's credentials reach today?
vxcli auth whoami
vxcli node list                      # every node the identity can see
vxcli networks list                  # every reachable network surface

# 2. What is actually exposed to the internet right now?
vxcli networks port-check 443 --host <your-edge-host>
vxcli networks security-audit --host <host> --ssh-user ubuntu --key-pair-name <key>

# 3. How many long-lived credentials exist, and when were they last rotated?
#    (If this number is > 0, Step 2 is your entire month.)

Write three numbers where the team can see them.

MetricDay 0Day 90 target
Resources reachable by one compromised employee identity____≤ 3
Long-lived (non-expiring) credentials in production____0
Mean time from "policy change committed" to "enforced everywhere"____< 60 s

Everything below moves those three numbers. If a proposed control does not move one of them, it is not a priority. Say that out loud in planning.

Note on commands. Examples use the vxcli command groups shipped today — auth, node, networks, vpn, workspace, audit, agentcontrol. Flags evolve; run vxcli <group> --help for the authoritative list, or read the CLI reference.

Step 1 — Identity: phishing-resistant MFA and enforced SSO

Threat closed: stolen credentials, which sit at or near the top of initial-access vectors in every DBIR ever published, plus the MFA-fatigue and adversary-in-the-middle attacks that defeat push and SMS.

TOTP is a floor, not a target. WebAuthn/FIDO2 hardware-backed authenticators are the only phishing-resistant factor — they cryptographically bind to the origin, so a proxy phishing page gets nothing. This is what OMB M-22-09 mandates for federal agencies and what NIST SP 800-63B rates highest.

Dashboard: Settings → Security → Multi-Factor Authentication → Require MFA for all members → allowed factors WebAuthn, TOTP. Uncheck SMS; it is a SIM-swap away from useless.

SSO federation: Settings → Organization → Single Sign-On. VxCloud supports SAML 2.0 and OpenID Connect against Okta, Microsoft Entra ID, Google Workspace, JumpCloud and OneLogin.

# Reference SAML config — values come from your IdP's metadata document
sso:
  protocol: saml2
  entity_id:  https://vxcloud.io/sso/saml/<account-id>
  acs_url:    https://vxcloud.io/sso/saml/<account-id>/acs
  idp_metadata_url: https://<your-idp>/app/<app-id>/sso/saml/metadata
  name_id_format: EmailAddress
  attribute_mapping:
    email:  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
    groups: "http://schemas.xmlsoap.org/claims/Group"
  enforce_sso: true            # the setting that actually matters
  disable_password_login: true # and this one
  scim_provisioning: true      # deprovision on termination, automatically

enforce_sso and disable_password_login are the two lines that make this a control. Without them you have added a convenient login method next to an insecure one, and attackers will use the second. SCIM matters because the alternative — manual deprovisioning — is how ex-employees keep production access for eleven months.

Prove it. Try a password login for a federated user; it must be rejected outright. Try an API call with a session that never satisfied MFA; it must 401 or 403. Deactivate a user in the IdP and confirm SCIM revokes access within your SLA — and that their live session dies, not just their next login. If that last test fails, your identity pillar is decorative.

Step 2 — Execute every long-lived credential in your estate

Threat closed: the credential that outlives its purpose. Colonial Pipeline was a dormant VPN account with no MFA. Your equivalent is a PROD_API_KEY created in 2022 by someone who now works elsewhere.

Rule, no exceptions: every credential expires. If a credential cannot expire, it is not a credential — it is a permanent backdoor with a friendly name.

VxCloud API keys are environment-prefixed so blast radius is legible in a single log line, stored only as a SHA-256 hash — the plaintext is shown once and never again — and carry real constraints.

PrefixEnvironmentShould ever touch customer data?
xc_live_ProductionYes — treat as radioactive
xc_stg_StagingNo
xc_dev_DevelopmentNo
xc_sbx_SandboxNo
xc_prev_Preview / ephemeralNo
{
  "name": "ci-deploy-web",
  "environment": "xc_live_",
  "expires_at": "2026-11-05T00:00:00Z",
  "is_read_only": false,
  "allowed_services":    ["deployments", "containers", "networks"],
  "disallowed_services": ["billing", "iam", "vaults", "agentcontrol"],
  "allowed_ips":  ["203.0.113.0/24"],
  "rate_limit":   600,
  "scopes": ["deploy:write", "containers:read", "networks:read"]
}

Note expires_at: 90 days, not "never." Never is not a duration.

Then bind automation to service accounts, not humans. A personal token running your deployment pipeline is a resignation letter away from an outage — and it attributes machine actions to a human in your audit log, which corrupts every investigation you will ever run.

# Automation identity — no console login, no password, no human owner
vxcli iam service-account create --name ci-github-actions \
  --policy ci-deploy-minimal --expires 90d

Better still: no stored key at all. If your CI supports OIDC federation — GitHub Actions, GitLab and Buildkite all do — exchange a short-lived workload token for temporary credentials and store nothing.

# .github/workflows/deploy.yml — zero stored secrets
permissions:
  id-token: write          # request an OIDC token from GitHub
  contents: read
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Exchange OIDC token for short-lived VxCloud credentials
        run: |
          vxcli auth login --oidc \
            --token "$ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
            --audience vxcloud.io --ttl 15m
      - uses: prodxcloud/vxcloud-deploy-action@v1

A 15-minute credential exfiltrated at 02:00 is worthless by 02:16. That is the whole game.

Prove it.

# Read-only key must fail a write:
curl -sS -X POST https://<node>/api/v2/deployments \
  -H "Authorization: Bearer $READONLY_KEY" -d '{}' | jq .   # expect 403

# Key from an unlisted IP must fail:
curl -sS https://<node>/api/v2/nodes -H "Authorization: Bearer $CI_KEY"  # expect 403 off-range

Do these tests; do not assume them. "Scopes stored but never enforced" is one of the most common findings in real IAM audits — the columns exist, the UI shows a read-only badge, and the middleware never reads them. A read-only key that can write is worse than no key management at all, because it manufactures false confidence.

Step 3 — A resource grammar and real least-privilege policy

Threat closed: the wildcard permission granted at 2 a.m. to unblock a deploy, still there three years later.

You cannot write least-privilege policy until every resource has a name you can pattern-match. This is the single most irreversible decision in a Zero Trust programme: pick the grammar before you have twenty resource types, because retrofitting it breaks every stored policy, reference and audit correlation you have written.

vxarn:vxcloud:<service>:<region>:<account-id>:<resource-type>/<resource-id>

vxarn:vxcloud:vpn:us-east-1:472910538812:server/3ea2a6b0-6e49-4684-a979-7a51b68acb45
vxarn:vxcloud:agentcontrol:*:472910538812:agent/bd052eca-3f9a-4e34-ae6f-5045659b6ad2
vxarn:vxcloud:databases:eu-west-1:472910538812:instance/prod-orders

The policy document is JSON, AWS-shaped, deliberately a strict subset — because a policy language nobody can read is a policy language nobody reviews.

{
  "Version": "2026-08-03",
  "Statement": [
    {
      "Sid": "AppTeamReadProdInfra",
      "Effect": "Allow",
      "Action": ["vpn:List*", "vpn:Get*", "databases:Describe*", "containers:Get*"],
      "Resource": "vxarn:vxcloud:*:*:472910538812:*"
    },
    {
      "Sid": "DeployOnlyToStaging",
      "Effect": "Allow",
      "Action": ["deployments:Create", "deployments:Update"],
      "Resource": "vxarn:vxcloud:deployments:*:472910538812:*",
      "Condition": { "StringEquals": { "vx:Environment": "STAGING" } }
    },
    {
      "Sid": "NeverDeleteInProduction",
      "Effect": "Deny",
      "Action": ["*:Delete*", "*:Terminate*", "*:Destroy*"],
      "Resource": "*",
      "Condition": { "StringEquals": { "vx:Environment": "PRODUCTION" } }
    },
    {
      "Sid": "RequireMfaForSecrets",
      "Effect": "Deny",
      "Action": ["vaults:*", "iam:*"],
      "Resource": "*",
      "Condition": { "Bool": { "vx:MultiFactorAuthPresent": "false" } }
    }
  ]
}

Three rules make this work in production:

  1. Evaluation order is explicit Deny → Allow → implicit Deny. An explicit Deny cannot be overridden by any subsequent Allow. That is why NeverDeleteInProduction is safe to grant broadly — it is a floor nobody can dig under, including an account admin having a bad day.
  2. Permissions are additive across scopes — account → workspace → resource. Grant broad read at account level, narrow write at workspace level.
  3. RequireMfaForSecrets is the pattern to copy everywhere. Condition-gate your highest-value actions on session properties: MFA presence, device compliance, source network, time of day. That is NIST's "dynamic policy" tenet, written down in something a machine enforces.

Prove it.

vxcli iam policy simulate \
  --principal "vxarn:vxcloud:iam::472910538812:user/35" \
  --action    "databases:DeleteInstance" \
  --resource  "vxarn:vxcloud:databases:eu-west-1:472910538812:instance/prod-orders"
# expect: DENY  (matched Sid=NeverDeleteInProduction)

Then run the review that actually finds the rot: diff permissions granted against permissions used over the last 90 days, from the audit log. Everything granted-and-never-used is a permission to revoke. Schedule it quarterly; it is the highest-yield hour of security work available to you.

Step 4 — Secrets: Vault, just-in-time, never in code

Threat closed: secret sprawl. .env files, CI variables, that one Confluence page, a Slack DM from 2023, and the notebook on a laptop in an airport lounge.

Every VxCloud workspace gets a per-tenant HashiCorp Vault namespace — KVv2, AppRole and JWT auth — with secrets resolved just-in-time at deploy and scrubbed from container environments afterwards.

# Store — value comes from stdin, never from shell history or a flag
vxcli workspace store-credential --name PROD_DB_PASSWORD --stdin --rotate
vxcli workspace store-git-credentials --provider github --stdin

# Read — every read is policy-checked and lands in the audit log
vxcli workspace get-credential --name PROD_DB_PASSWORD
# Vault policy for a deploy service account — read one path, nothing else.
path "secret/data/workspaces/<workspace-id>/deploy/*" {
  capabilities = ["read"]
}
path "secret/data/workspaces/<workspace-id>/*" {
  capabilities = ["deny"]        # explicit, so a future wildcard cannot widen it
}

Four rules. Dynamic secrets over static ones — Vault's database engine mints a per-session DB user with a 1-hour TTL, so there is no "database password" to steal, rotate or leak; the concept ceases to exist. Private keys are generated server-side and never leave Vault — if a human can copy the private key, so can malware on that human's laptop. Key IDs in config, values at runtime — your repo should contain PROD_DB_PASSWORD as a reference. And customer-managed keys where it matters, via AWS KMS, Azure Key Vault, GCP KMS or self-hosted Vault Transit, so your provider never holds the only copy of the key protecting your data.

Prove it.

# Scan history for committed secrets (do this in CI, on every PR):
gitleaks detect --source . --redact
trufflehog filesystem . --only-verified

# Confirm secrets are absent from the running container's environment:
docker exec <container> env | grep -iE 'password|secret|token|key'   # expect: nothing

Step 5 — Micro-segmentation: default deny, both directions

Threat closed: lateral movement — MITRE ATT&CK TA0008. This is the step that collapses blast radius, and the one most teams skip because it is the one that briefly breaks things.

Default-deny inbound is table stakes. Default-deny egress is where the actual value is — it is what stops data exfiltration and command-and-control after the initial compromise you failed to prevent.

# ufw — deny everything, allow the minimum, verify
ufw default deny incoming
ufw default deny outgoing                   # the line nobody wants to write
ufw allow in on wg0 from 10.80.0.0/16 to any port 8443 proto tcp
ufw allow out to 10.80.0.0/16               # mesh peers
ufw allow out 53/udp                        # DNS (pin to your resolver)
ufw allow out 443/tcp to 203.0.113.10       # package mirror, by IP, not "anywhere"
ufw --force enable && ufw status numbered
# /etc/ssh/sshd_config.d/99-zero-trust.conf
PasswordAuthentication no
PermitRootLogin no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
AllowGroups ssh-prod
ClientAliveInterval 300
ClientAliveCountMax 2
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding no          # stop SSH being an ad-hoc VPN into your estate
MaxAuthTries 3
LogLevel VERBOSE               # logs the key fingerprint used — you need this

AllowTcpForwarding no is the sleeper. An engineer with SSH and TCP forwarding has a personal, unlogged tunnel that bypasses every network control you just configured.

# 1. Default-deny EVERYTHING in the namespace — ingress and egress.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: default-deny-all, namespace: prod }
spec:
  podSelector: {}
  policyTypes: [Ingress, Egress]
---
# 2. Then allow exactly one path: api → postgres, on one port.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: api-to-postgres, namespace: prod }
spec:
  podSelector: { matchLabels: { app: postgres } }
  policyTypes: [Ingress]
  ingress:
    - from:
        - podSelector: { matchLabels: { app: api, tier: backend } }
      ports: [{ protocol: TCP, port: 5432 }]
---
# 3. DNS is the exception everyone forgets — allow it explicitly or nothing resolves.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: allow-dns-egress, namespace: prod }
spec:
  podSelector: {}
  policyTypes: [Egress]
  egress:
    - to: [{ namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: kube-system } } }]
      ports: [{ protocol: UDP, port: 53 }, { protocol: TCP, port: 53 }]

Kubernetes NetworkPolicy is an additive allow-list on top of a deny baseline: the deny policy establishes the baseline, each subsequent policy opens one door. NetworkPolicy is CIDR and label-based; for genuine identity-based segmentation, layer Step 6's authorisation policies on top.

On the managed side, VxCloud Networking gives you per-tenant VPCs across AWS, Azure, GCP, Alibaba, Linode and bare metal with default-deny security groups — ports opened explicitly, per deployment, and never "temporarily" left open, because the wizard writes the rule and Terraform reviews it.

Prove it.

# From a pod that should NOT have access — this must hang and time out:
kubectl -n prod run probe --rm -it --image=busybox --restart=Never \
  -- sh -c 'nc -zvw3 postgres 5432; echo "exit=$?"'     # expect: failure

# From the api pod — this must succeed:
kubectl -n prod exec deploy/api -- nc -zvw3 postgres 5432

# Egress containment — this must fail from a production pod:
kubectl -n prod exec deploy/api -- curl -m 5 https://example.com   # expect: timeout

If curl https://example.com succeeds from your production pod, you do not have egress control, and exfiltration is a one-liner for whoever gets in.

Step 6 — mTLS and workload identity with SPIFFE/SPIRE

Threat closed: the static service-to-service API key that has been in Git since 2022 and authenticates half your microservices.

Workload identity with SPIFFE and SPIRE — a shared static key replaced by one-hour SVIDs, mutual TLS, and authorisation by principal rather than IP

Human identity is mostly solved. Workload identity is where 2026's breaches live. The fix is SPIFFE/SPIRE, CNCF-graduated, where every workload gets a short-lived cryptographic identity (an SVID) and proves it via mutual TLS, with no shared secret anywhere in the system.

# spire-server.conf
server {
  trust_domain = "prod.vxcloud.io"
  data_dir     = "/opt/spire/data/server"
  ca_ttl       = "24h"
  default_x509_svid_ttl = "1h"     # short. shorter than you're comfortable with.
}
plugins {
  NodeAttestor "k8s_psat" { plugin_data { clusters = { "prod" = { service_account_allow_list = ["spire:spire-agent"] } } } }
  KeyManager   "disk"     { plugin_data { keys_path = "/opt/spire/data/server/keys.json" } }
}
# Register a workload identity, attested by k8s labels — not by a secret
spire-server entry create \
  -spiffeID  spiffe://prod.vxcloud.io/ns/prod/sa/api \
  -parentID  spiffe://prod.vxcloud.io/ns/spire/sa/spire-agent \
  -selector  k8s:ns:prod -selector k8s:sa:api -ttl 3600

Then enforce mTLS at the mesh and authorise by identity, not by IP (Istio security).

# STRICT mTLS mesh-wide — plaintext is refused, not merely discouraged
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata: { name: default, namespace: istio-system }
spec:
  mtls: { mode: STRICT }
---
# Authorization by workload identity + method + path. IPs appear nowhere.
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata: { name: postgres-access, namespace: prod }
spec:
  selector: { matchLabels: { app: postgres } }
  action: ALLOW
  rules:
    - from: [{ source: { principals: ["cluster.local/ns/prod/sa/api"] } }]
      to:   [{ operation: { ports: ["5432"] } }]

mtls: { mode: STRICT } is the line. PERMISSIVE accepts plaintext and is a migration setting — teams leave it on for eighteen months and believe they have mTLS. Set a calendar reminder to flip it, and verify.

# TLS 1.3 only, modern ciphers, HSTS — RFC 8446
ssl_protocols TLSv1.3;
ssl_ciphers TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256;
ssl_prefer_server_ciphers off;
ssl_session_tickets off;
ssl_stapling on; ssl_stapling_verify on;

add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header Content-Security-Policy "default-src 'self'; frame-ancestors 'none'; object-src 'none'" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;

VxCloud terminates TLS 1.3 with HSTS and forward secrecy on public endpoints, runs mTLS between control-plane services and tenant nodes, and issues and rotates certificates automatically on managed L7 load balancers — so "the cert expired on a Saturday" stops being an incident category.

Prove it.

# Plaintext to a STRICT-mTLS service must be refused:
kubectl -n prod exec deploy/probe -- curl -m 5 http://postgres:5432   # expect: failure

# Confirm the negotiated protocol and that TLS 1.2 is actually gone:
openssl s_client -connect api.example.com:443 -tls1_3 </dev/null | grep -E 'Protocol|Cipher'
openssl s_client -connect api.example.com:443 -tls1_2 </dev/null      # expect: handshake failure

# Confirm SVIDs are short-lived (notAfter ~1h out, not ~1y):
spire-agent api fetch x509 -write /tmp/ && \
  openssl x509 -in /tmp/svid.0.pem -noout -dates -ext subjectAltName

Or use Guardian's Ciphers, SSL/TLS and Headers tools to check the same things continuously from outside — which is the perspective an attacker actually has.

Step 7 — Encrypted transport that grants no trust

You still need site-to-site, cloud-to-cloud and edge/OT connectivity. That is legitimate and permanent. What is not legitimate is treating the far end of the tunnel as trusted.

WireGuard is the correct primitive: ~4,000 lines, formally analysed, in-kernel since Linux 5.6, modern crypto with no negotiable cipher suites to downgrade.

# A multi-cloud encrypted mesh, provisioned from one control plane
vxcli vpn network create --name prod-mesh --cidr 10.80.0.0/16 --topology mesh
vxcli vpn peer add prod-mesh --name aws-use1 --cloud aws --region us-east-1
vxcli vpn peer add prod-mesh --name gcp-euw1 --cloud gcp --region europe-west1
vxcli vpn tunnel add --name dc-nyc --protocol ipsec --remote 198.51.100.7
vxcli vpn client config aws-use1 --format wg-quick
vxcli vpn monitor prod-mesh --watch          # live RTT, throughput, failover
# wg0.conf — generated by: vxcli vpn client config aws-use1 --format wg-quick
[Interface]
Address    = 10.80.1.4/32
PrivateKey = <resolved from Vault at render time — never committed>
DNS        = 10.80.0.1
MTU        = 1420

[Peer]
PublicKey    = <peer public key>
Endpoint     = vpn-use1.example.net:51820
# AllowedIPs is your ACL. Route the mesh, NOT the internet, NOT 0.0.0.0/0.
AllowedIPs   = 10.80.0.0/16
PersistentKeepalive = 25

AllowedIPs is a cryptographic access-control list, not a routing hint — WireGuard drops packets whose source does not match the peer's allowed range. Setting 0.0.0.0/0 on a server-to-server tunnel turns a segment boundary into a full-mesh highway. Keep it tight.

VxCloud's VPN stack covers WireGuard, OpenVPN, IPSec/IKEv2, L2TP, SSTP, OpenConnect and SoftEther from one control plane, with mesh, hub-and-spoke and site-to-site topologies across AWS, Azure, GCP and on-prem, plus config download, lifecycle control and per-peer monitoring. The tunnel carries bytes. Authorisation still happens per request, at the PEP, in Step 8. Both layers. Always.

Step 8 — Put an identity-aware proxy in front of every internal app

Threat closed: "internal" apps reachable purely because someone is on the VPN — admin dashboards, Grafana, staging, the internal wiki, that Jenkins instance nobody has patched since the pandemic.

The identity-aware proxy is the Policy Enforcement Point from NIST SP 800-207. It sits in the data path, authenticates the user, validates the device, evaluates policy, and forwards one request to one application. It never grants a routable network path. Google ran their entire workforce this way as BeyondCorp — proven at planetary scale, not a thought experiment.

# Reference IAP configuration for an internal app
app:
  name: internal-grafana
  upstream: http://10.80.4.12:3000       # reachable ONLY from the proxy
  public_hostname: grafana.internal.example.com
policy:
  require:
    - authenticated: true
    - mfa: webauthn                       # phishing-resistant, per session
    - groups: ["sre", "platform-oncall"]  # from your IdP, not a local list
    - device:
        managed: true
        disk_encryption: true
        os_patch_age_days: { lte: 30 }
        edr_running: true
  session:
    max_lifetime: 8h
    reauth_after_idle: 30m
    revoke_on_posture_change: true        # the line that beats session-theft attacks
  audit: full_request_metadata

revoke_on_posture_change: true is what makes this per-request rather than per-session trust. Citrix Bleed did not defeat MFA — it stole a post-MFA session. If a compromised device loses compliance mid-session and access dies mid-session, that class of attack stops working.

Order of operations that works: start with two or three boring, popular internal apps. The wiki. The dashboard. Adoption becomes organic, failure modes are cheap, and by the time you reach the scary app your runbooks are already written.

Step 9 — Policy as code: OPA/Rego in CI

Threat closed: the policy that exists in a wiki, enforced by nothing, reviewed by no one, and true only on the day it was written.

Open Policy Agent gives you one policy language across ingress proxies, service-mesh sidecars, Kubernetes admission control and application authorisation — version-controlled in Git, unit-tested in CI, distributed to every PEP.

package vxcloud.authz

import rego.v1

default allow := false

# Baseline: authenticated + phishing-resistant MFA + compliant device.
allow if {
    input.subject.authenticated
    input.subject.mfa in {"webauthn", "totp"}
    device_compliant
    action_permitted
    not explicitly_denied
}

device_compliant if {
    input.device.managed
    input.device.disk_encrypted
    input.device.os_patch_age_days <= 30
    input.device.edr_running
}

# Production writes require hardware-backed MFA and a corporate network egress.
action_permitted if {
    input.resource.environment == "PRODUCTION"
    input.action.verb in {"create", "update", "delete"}
    input.subject.mfa == "webauthn"
    net.cidr_contains("203.0.113.0/24", input.subject.source_ip)
    input.subject.groups[_] == "prod-writers"
}

# Non-production is looser, deliberately — friction where risk is.
action_permitted if {
    input.resource.environment != "PRODUCTION"
    input.action.verb in {"read", "list", "create", "update"}
}

# Explicit deny always wins — mirrors the JSON policy engine's evaluation order.
explicitly_denied if {
    input.resource.classification == "restricted"
    not input.subject.groups[_] == "data-restricted-approved"
}
# policy_test.rego — the part teams skip, which is why their policy is wrong
package vxcloud.authz_test

import data.vxcloud.authz
import rego.v1

test_prod_write_denied_without_webauthn if {
    not authz.allow with input as {
        "subject":  {"authenticated": true, "mfa": "totp", "groups": ["prod-writers"], "source_ip": "203.0.113.9"},
        "device":   {"managed": true, "disk_encrypted": true, "os_patch_age_days": 5, "edr_running": true},
        "resource": {"environment": "PRODUCTION"}, "action": {"verb": "update"},
    }
}

test_restricted_data_denied_even_for_admin if {
    not authz.allow with input as {
        "subject":  {"authenticated": true, "mfa": "webauthn", "groups": ["admin"], "source_ip": "203.0.113.9"},
        "device":   {"managed": true, "disk_encrypted": true, "os_patch_age_days": 1, "edr_running": true},
        "resource": {"environment": "PRODUCTION", "classification": "restricted"}, "action": {"verb": "read"},
    }
}

test_unmanaged_device_always_denied if {
    not authz.allow with input as {
        "subject":  {"authenticated": true, "mfa": "webauthn", "groups": ["prod-writers"], "source_ip": "203.0.113.9"},
        "device":   {"managed": false}, "resource": {"environment": "STAGING"}, "action": {"verb": "read"},
    }
}
# .github/workflows/policy.yml — policy fails CI like any other broken code
name: policy
on: [pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: curl -sL -o opa https://openpolicyagent.org/downloads/latest/opa_linux_amd64_static && chmod +x opa
      - run: ./opa fmt --fail --diff policies/
      - run: ./opa check --strict policies/
      - run: ./opa test policies/ -v

Target: policy commit → enforced everywhere in under 60 seconds. That number is your organisation-wide revocation speed. When an engineer leaves at 17:00 on a Friday, it is the difference between "handled" and "hoping."

Step 10 — Hash-chained audit, streamed to your SIEM

Threat closed: two things at once — the attacker who edits the log to erase themselves, and the auditor who asks for six months of evidence you cannot produce.

An append-only hash-chained audit log — deleting an event breaks the chain and raises an alert, while the live stream fans out to Splunk, Datadog and Elasticsearch

NIST tenet 7 is explicit: collect as much information as possible about asset state, network traffic and access requests, and use it to improve posture. Zero Trust without telemetry is theatre, because you cannot write dynamic policy against signals you never collected.

VxCloud's audit log is append-only and cryptographically hash-chained, with per-event tenant, actor, resource and IP attribution, retention configurable from 90 days to 7 years, and both a real-time WebSocket stream and a REST backfill API.

# 1. Issue a read-only, TTL-bound streaming token
#    Dashboard → Settings → Integrations → Audit Streaming → New token (default 30d)

# 2. Stream live, filter to the events that matter
vxcli audit stream --token aud_… --json | jq 'select(.decision=="DENY")'

# 3. Forward to Splunk HEC
vxcli audit stream --token aud_… --json | while read -r line; do
  curl -sH "Authorization: Splunk $SPLUNK_HEC" \
       -d "{\"event\": $line}" \
       "https://splunk.example.com:8088/services/collector/event"
done

# 4. Backfill a SIEM that just came online
vxcli audit events --since 2026-01-01 --until 2026-08-01 --json > backfill.jsonl

Detections to write on day one — cheap, and they catch real intrusions:

DetectionWhy it fires on a real intrusion
Any DENY on a production resourceLateral movement in a Zero Trust network looks like denied requests
Secret read outside business hours from a new IPCredential harvesting after laptop compromise
New API key created, then used from a different ASN within 5 minKey exfiltration, near-universally
Policy modified without a linked Git commitSomeone bypassed change control — or is not an employee
Service account authenticating from a residential IP rangeMachine credential now in human hands
MFA method removed or downgradedClassic account-takeover persistence step
Audit hash-chain verification failurePage immediately. Someone is editing history

That last row is why hash chaining matters more than people assume. A mutable log is a log an attacker cleans on the way out. Tamper-evidence converts your log from an artefact the attacker controls into one they cannot quietly touch.

Step 11 — Continuous validation: attack your own surface, weekly

Threat closed: the exposure you do not know about, which by definition is not covered by any policy you wrote.

Zero Trust policy only governs assets you know exist. Attack-surface discovery is therefore not "a pentest thing" — it is a prerequisite for the policy pillar, and it must run on a schedule, not on a project plan.

VxCloud's Guardian console ships 22 reconnaissance and posture tools you run against your own estate from the same console that provisions it.

CategoryToolsWhat it catches
ExposurePort Scan, Vuln Scan, Ping, TracerouteThe staging box someone gave a public IP "for a demo" in March
Crypto / TLSSSL/TLS, Ciphers, Certs (Certificate Transparency)Expiring certs, TLS 1.0 still enabled, certificates issued for your domain that you did not request
DNSDNS, DNS×3 multi-resolver, DNS Propagation, Reverse DNSResolver tampering, stale records pointing at reclaimable cloud IPs (subdomain takeover)
SurfaceSubdomains, Tech Detect, Headers, WHOISForgotten hosts, missing HSTS/CSP, leaked stack versions
ReputationIP Rep, Blacklist, VPN/Proxy detect, Routing (BGP/ASN), ThreatsYour egress IP on a DNSBL, hostile ASNs, IOC enrichment
CredentialDark Web exposureYour users' credentials in breach corpora — before they are used against you
# Add these to a weekly scheduled job. Not quarterly. Weekly.
vxcli networks port-check 443 --host <edge-host>
vxcli networks security-audit --host <host> --ssh-user ubuntu --key-pair-name <key>

Layer on continuous runtime posture with Observability — Prometheus, blackbox probing, native dashboards — so "the control silently stopped working in April" is something you find out in April.

Certificate Transparency monitoring deserves special mention. A certificate issued for your domain that you did not request is a five-alarm signal of DNS or registrar compromise, it is publicly logged, and almost nobody watches for it. Watch for it.

Step 12 — All of it in Terraform, reviewed in pull requests

Threat closed: configuration drift, and the "temporary" firewall rule from 2024 that nobody can now explain or safely delete.

If your Zero Trust posture lives in a web console, it is one distracted click from gone, with no diff, no reviewer and no rollback. Infrastructure as code makes security posture a reviewable artefact.

terraform {
  required_providers {
    vxcloud = { source = "prodxcloud/vxcloud" }
  }
}

provider "vxcloud" {
  # Token from environment / OIDC exchange — never hardcoded, never committed
  api_token = var.vxcloud_token
}

resource "vxcloud_vm" "api" {
  name          = "api-prod-use1"
  cloud         = "aws"
  region        = "us-east-1"
  size          = "t3.medium"
  # No public IP. Reachable only through the mesh and the IAP.
  assign_public_ip = false
  tags = { environment = "PRODUCTION", data_classification = "restricted" }
}

Then gate the plan with policy, so an insecure change cannot merge.

package terraform.security

import rego.v1

deny contains msg if {
    r := input.resource_changes[_]
    r.type == "vxcloud_vm"
    r.change.after.assign_public_ip == true
    r.change.after.tags.environment == "PRODUCTION"
    msg := sprintf("%s: production VMs must not have a public IP", [r.address])
}

deny contains msg if {
    r := input.resource_changes[_]
    r.change.after.ingress[_].cidr_blocks[_] == "0.0.0.0/0"
    msg := sprintf("%s: 0.0.0.0/0 ingress is never approved — use the IAP", [r.address])
}
terraform plan -out=tfplan && terraform show -json tfplan > plan.json
opa eval --format pretty --data policies/terraform.rego --input plan.json \
  'data.terraform.security.deny'    # non-empty ⇒ fail the build

The VxCloud Terraform provider covers multi-cloud VMs, networks and platform resources across AWS, Azure, GCP, Alibaba, Linode and Vultr — so your posture is one terraform plan away from being provable, and one git log away from being explicable.

The verification matrix: prove each control or delete it

Print this. Run it quarterly. A control nobody tests is a control that has already failed and has not told you yet.

#ControlTestPass criterion
1Phishing-resistant MFAPassword-only login to productionRejected
2SSO enforcedFederated user attempts local password loginRejected
3SCIM deprovisioningDeactivate user in IdPAccess + live session die within SLA
4Key expiryUse a key past expires_at401
5Key IP bindingCall from an IP outside allowed_ips403
6Read-only scopePOST with a read-only key403
7Explicit deny precedenceAdmin attempts a production deleteDENY, matching the Deny Sid
8MFA-gated secretsRead a secret in a non-MFA sessionDENY
9No secrets in codegitleaks / trufflehog over full history0 verified findings
10Secrets absent at runtimedocker exec <c> env piped to grep -i secretNo output
11Ingress segmentationUnauthorised pod → database portTimeout
12Egress containmentProduction pod → arbitrary internet hostTimeout
13STRICT mTLSPlaintext HTTP to a mesh serviceConnection refused
14Short-lived SVIDsInspect certificate notAfter≤ 1 hour
15TLS flooropenssl s_client -tls1_2Handshake failure
16Security headersGuardian Headers toolHSTS, CSP, XFO, XCTO, Referrer, Permissions all present
17Tunnel scopingInspect AllowedIPs on every peerNo 0.0.0.0/0 on server-to-server
18Session revocationBreak device posture mid-sessionAccess revoked without re-login
19Policy propagationCommit a policy change, time enforcement< 60 s
20Audit integrityVerify the hash chainUnbroken
21SIEM ingestionTrigger a test eventVisible in SIEM < 60 s
22Attack surfaceWeekly Guardian sweepNo unknown exposed services
23Permission hygieneDiff granted vs used over 90 daysUnused permissions revoked
24Blast radiusTabletop: engineer laptop compromised≤ 3 reachable resources

Twelve misconfigurations that silently switch Zero Trust off

Every one of these has been observed in a real environment whose owners believed they were Zero Trust.

  1. PERMISSIVE mTLS left on after migration. Plaintext still accepted. Eighteen months of false confidence.
  2. AllowedIPs = 0.0.0.0/0 on a server-to-server WireGuard peer. You built a full-mesh highway and called it a segment.
  3. Default-deny inbound, wide-open egress. You blocked the burglar's front door and left the loading bay open for the furniture.
  4. The break-glass account. No MFA, memorable password, "for emergencies." It is the emergency.
  5. Scopes stored but never enforced. The UI shows "read-only." The middleware never reads the column.
  6. Device posture checked at enrolment only. Compliance is continuous or it is fiction.
  7. AllowTcpForwarding yes on SSH. Every engineer has a personal, unlogged tunnel around your network controls.
  8. Wildcard service-account permissions added at 2 a.m. to fix an outage, never revisited.
  9. Audit log in a mutable table. The attacker's last act is DELETE FROM audit WHERE actor = ….
  10. Policy in a wiki. Not in the data path ⇒ not a control ⇒ a wish with formatting.
  11. The IdP itself lacking MFA on admin accounts. You federated everything to one system and left its own front door open. Attackers go for the IdP first because everyone does this.
  12. Zero Trust everywhere except the AI stack. Agents holding long-lived provider keys, unauthenticated model endpoints, RAG retrieval that trusts the UI to filter by tenant. Treat every agent as a principal: service-account identity, vxarn-scoped policy, expiring tokens, per-call tool authorisation, every action in the audit log. Agent Control governs agents, models, datasets and endpoints under the same grammar and the same engine as your VMs — which is the entire point.

The 90-day checklist

WindowDo thisMoves which metric
Week 1Baseline blast radius; enable audit streaming to SIEM; run a full Guardian sweepVisibility — everything depends on it
Week 2WebAuthn MFA enforced org-wide; enforce_sso + disable_password_login; SCIM onCredential theft
Week 3Inventory every long-lived credential; convert automation to service accounts; expiry on all keysLong-lived credentials → 0
Week 4Adopt the resource grammar (vxarn); write and simulate the first 5 policiesMakes least privilege expressible
Weeks 5–6Secrets into Vault; scrub Git history; gitleaks in CISecret sprawl
Weeks 7–8Default-deny ingress and egress on one production segment; K8s NetworkPolicyBlast radius — the big one
Weeks 9–10STRICT mTLS + SPIFFE for all new services; TLS 1.3 floor at the edgeEast-west interception, static service keys
Weeks 11–12Two internal apps behind the identity-aware proxy; device posture as a required signalVPN dependency
Day 90Re-measure blast radius; run the 24-test verification matrix; publish the deltaProves the programme
Days 90–180Micro-segment the rest of production; decommission user-facing VPN; automate SOC 2 / ISO evidence from the audit logBlast radius, audit cost

FAQ

What's the single highest-value configuration change I can make this week? Phishing-resistant MFA (WebAuthn) enforced on every production path, with password login disabled. It closes the vector that appears in more incident reports than any other. Second place: default-deny egress on one production segment.

Do I need Kubernetes and a service mesh to do Zero Trust? No. Steps 1–4, 7, 8, 10, 11 and 12 apply to plain VMs. Micro-segmentation on VMs is ufw and security-group default-deny plus per-request authorisation at an identity-aware proxy. The mesh makes Step 6 easier; it is not a prerequisite.

Won't default-deny egress break everything? It will break some things, loudly, for about a week — which is the point: you are discovering undocumented dependencies you did not know production had. Roll it out on one segment, log denials before enforcing, allowlist what is real, then enforce.

How do I do this across AWS and Azure and GCP without four policy languages? One resource grammar, one policy engine, one audit stream — which is what VxCloud centralises. Four separate cloud-native IAM systems is not Zero Trust; it is four perimeters in a trench coat.

We're regulated and can't send data to a SaaS control plane. Then do not. Self-hosted VxCloud deploys into your own VPC or air-gapped on-prem, with BYO IdP, BYO secrets store and BYO SIEM. Data residency is a Zero Trust data-pillar requirement, not a preference.

How do I configure Zero Trust for AI agents? Identically to any other principal, because that is what they are: service-account identity, vxarn-scoped least privilege, short-lived tokens, per-call tool authorisation, per-tenant isolation on the vector store, an identity-aware proxy in front of every inference endpoint, and every tool call in the audit log.

What proves to an auditor that these controls work? The 24-test verification matrix above, plus audit-log evidence generated from the hash-chained stream and mapped to SOC 2 CC6/CC7 and ISO 27001 A.5/A.8. Generated evidence beats screenshots, at a fraction of the labour cost.

How long until we're "done"? You are never done — Zero Trust is a continuous posture, not a milestone. But you should see measurable blast-radius reduction in 90 days and mature posture in 180. If your programme has no measurable delta at day 90, it is a governance failure, not a technology failure.

References

  1. NIST, SP 800-207: Zero Trust Architecturecsrc.nist.gov
  2. NIST NCCoE, SP 1800-35: Implementing a Zero Trust Architecturenccoe.nist.gov
  3. CISA, Zero Trust Maturity Model v2.0cisa.gov
  4. US OMB, M-22-09: Federal Zero Trust Strategywhitehouse.gov
  5. Executive Order 14028federalregister.gov
  6. US DoD CIO, DoD Zero Trust Strategydodcio.defense.gov
  7. Verizon, Data Breach Investigations Reportverizon.com
  8. IBM Security, Cost of a Data Breach Reportibm.com
  9. Google, BeyondCorpresearch.google
  10. OpenSSH, sshd_config(5)man.openbsd.org
  11. SPIFFE / SPIRE — spiffe.io
  12. Open Policy Agent, Rego policy languageopenpolicyagent.org
  13. IETF, RFC 8446: TLS 1.3rfc-editor.org
  14. NIST, SP 800-207Acsrc.nist.gov
  15. J. A. Donenfeld, WireGuard, NDSS 2017 — wireguard.com
  16. MITRE ATT&CK, Lateral Movement (TA0008)attack.mitre.org
  17. CISA, Emergency Directive 24-01 (Ivanti)cisa.gov
  18. CISA, Known Exploited Vulnerabilities Catalogcisa.gov
  19. NIST NVD, CVE-2023-4966 (Citrix Bleed)nvd.nist.gov
  20. NIST NVD, CVE-2018-13379 (FortiOS SSL VPN)nvd.nist.gov
  21. CIS, CIS Benchmarkscisecurity.org
  22. NIST, SP 800-63B: Digital Identity Guidelinespages.nist.gov
  23. OWASP, Secrets Management Cheat Sheetcheatsheetseries.owasp.org
  24. Cloud Security Alliance, Cloud Controls Matrix v4cloudsecurityalliance.org
  25. Kubernetes, Network Policieskubernetes.io
  26. Istio, Security: mutual TLS & authorization policyistio.io
  27. HashiCorp Vault, Database secrets enginedeveloper.hashicorp.com
  28. NIST, SP 800-53 Rev. 5csrc.nist.gov
  29. UK NCSC, Zero Trust Architecture Design Principlesncsc.gov.uk
  30. GitHub, Security hardening with OpenID Connectdocs.github.com
  31. VxCloud Terraform Provider — registry.terraform.io
  32. VxCloud Security & Compliance — vxcloud.io/pages/web/security

Configure it on a platform that was built for this

You can assemble every control above from a dozen vendors and spend two years integrating them. Or you can run them from one control plane that already speaks one resource grammar, one policy engine and one audit stream — across AWS, Azure, GCP, Alibaba, Linode, Vultr and your own bare metal, or fully self-hosted in your VPC or air-gapped facility.

VxCloud, by prodxcloud, ships the enforcement plane described in this playbook:

  • Identity — SAML 2.0 / OIDC SSO, WebAuthn + TOTP MFA, SCIM, vxarn-scoped policies with explicit-deny precedence
  • Secrets — per-workspace HashiCorp Vault namespace, just-in-time resolution, server-side key generation, CMK support
  • Network — per-tenant VPCs, default-deny security groups, managed L7 load balancers with automatic TLS, WAF, mTLS to tenant nodes
  • Transport — managed WireGuard, OpenVPN, IPSec/IKEv2, L2TP, SSTP, OpenConnect, SoftEther; mesh, hub-and-spoke, site-to-site
  • Visibility — Guardian's 22 recon and posture tools, Prometheus-backed observability, and an append-only, hash-chained audit log streaming to Splunk, Datadog or ELK
  • AI governance — agents, models, datasets and endpoints as first-class governed resources under the same policy engine
  • Everything as code — official Terraform provider, CLI parity, SDKs in Python, TypeScript, Go, C++ and Java

Get started:

Previously in this series: Zero Trust Architecture in 2026 — Your Perimeter Is Already Breached.

Zero TrustSecurityHardeningSPIFFEOPAKubernetesWireGuardTerraform

Keep Reading

Related articles

View all posts