Deploy a sovereign EU installation
Install MAQPNA on a cluster with the sovereign-eu profile, check agents against a sovereignty policy, run a session on an attested confidential VM, and verify evidence signed with keys you hold.
In this tutorial you build a production-like MAQPNA installation that keeps agents, data and keys in the EU. You
start from the chart's sovereign-eu profile, check your agents against the sovereignty policy before they reach
the cluster, install, run one agent on tier-2 (confidential VM) with hardware attestation, and verify the audit
ledger with your own signing key.
What you'll build#
flowchart TB
subgraph Git["Your workstation or CI"]
V["my-values.yaml"] --> VV["maqpna values validate<br/>--profile sovereign"]
M["Agent + ToolPolicy"] --> SC["maqpna sovereignty check"]
end
subgraph EU["EU cluster"]
SP["SovereigntyPolicy default"] --> OP["Operator admission"]
OP --> T2["Session on tier-2<br/>kata-qemu-snp"]
T2 -->|hardware report| AT["maqpna-attest"]
AT -->|appraise| TR["Your Trustee"]
AT -->|token sealed to TEE| T2
T2 --> GW["Gateway x3<br/>residency dialer"]
GW --> PG[("PostgreSQL<br/>in-country")]
GW --> WORM[("S3 Object Lock")]
GW --> SIEM["SIEM (TLS syslog)"]
end
K["Keys you hold"] --> GW
K --> ID["Identity broker"]
| Time | 2 to 3 hours, once the infrastructure below exists |
| Level | Advanced |
| You learn | The sovereign-eu profile, SovereigntyPolicy, maqpna sovereignty check, maqpna values validate --profile sovereign, tier-2 attestation, customer-held identity and checkpoint keys |
A sovereignty policy (SovereigntyPolicy) is the cluster-wide set of rules on jurisdiction, registries,
egress and attestation that every session is admitted against. A trust tier is the isolation level a session
runs at. Attestation is hardware proof that a tier-2 sandbox is a genuine confidential VM. The concepts are
in Sovereignty; this tutorial is the deployment.
Prerequisites#
This tutorial does not run on a laptop. The profile assumes real infrastructure, and you need all of it:
| You need | Used for |
|---|---|
| Kubernetes 1.30 or later in an EU region, and cluster-admin rights | The profile renders a ValidatingAdmissionPolicy |
| The upstream agent-sandbox controller (v1.0.x) and a CNI that enforces NetworkPolicy | Sandboxes and default-deny egress |
Nodes with AMD SEV-SNP or Intel TDX, Kata Confidential Containers, handler kata-qemu-snp, label maqpna.com/confidential=true; plus kata-fc and gVisor (runsc) nodes |
tier-2, tier-1 and tier-0 |
| A Trustee attestation service you run, and its token-signing JWKS | Appraising hardware reports |
| The launch measurements and init-data digests of your Kata guest image | Reference values; MAQPNA does not compute them |
| An in-country registry mirror holding the MAQPNA images | airgap.imageRegistry |
| PostgreSQL, S3-compatible storage with Object Lock, a TLS syslog SIEM and an OIDC identity provider, all in-country | State, WORM audit, SIEM and approver sign-in |
The maqpna CLI with its maqpna-install plugin, and helm and kubectl |
Every step |
REGISTRY below is the release registry listed on maqpna.com/download. To fill
the in-country mirror from a disconnected site, follow Air-gapped install.
Step 1: Get the chart and the profile#
Pull the chart into a working folder:
mkdir sovereign-eu && cd sovereign-eu
helm pull "oci://$REGISTRY/charts/maqpna" --version 2.1.0 --untar
maqpna verify release v2.1.0 --registry "$REGISTRY" # note the image digests to pin
The profile turns on mirror-only images, the sovereignty policy, PostgreSQL state, Trustee attestation, WORM
audit, a TLS SIEM sink, signed checkpoints, the eu-strict DLP profile, your own identity key, audit that fails
closed, three gateway replicas, OIDC-only admin access and three trust tiers. Replace every CHANGE-ME value.
Check it works#
maqpna values validate --chart ./maqpna -f maqpna/values-sovereign-eu.yaml --profile sovereign
OK: values valid (./maqpna/values.yaml, maqpna/values-sovereign-eu.yaml; schema maqpna/values.schema.json)
Step 2: Check your agent against the sovereignty policy#
Check agents before they reach the cluster. Create manifests/sovereignty.yaml, matching the sovereignty: block
you will set in your values (the chart renders it as the policy named default, the one the operator enforces):
apiVersion: maqpna.com/v1alpha1
kind: SovereigntyPolicy
metadata:
name: default
spec:
jurisdiction: EU
allowedJurisdictions: ["EU", "EU-DE", "EU-FR", "EU-NL"]
allowedRegistries: ["registry.eu.internal:5000/"]
allowedEgressHosts: ["*.svc", "*.svc.cluster.local", "*.tools.eu.internal", "idp.eu.internal"]
allowedEgressCIDRs: ["10.20.0.0/16"]
requireAttestationFor: ["confidential"]
keyCustody: pkcs11
telemetry: "off"
auditRetentionDays: 3650
enforcement: enforce
Create manifests/tier-2.yaml, the tier-2 trust tier as the profile creates it:
apiVersion: maqpna.com/v1alpha1
kind: TrustTier
metadata:
name: tier-2
spec:
runtimeClassName: kata-qemu-snp
isolation: confidential
attestationRequired: true
jurisdiction: "EU"
egress: gateway-only
allowedNodeLabels:
topology.kubernetes.io/region: eu-central-1
Create the agent and its policy in manifests/case-agent.yaml. It reads cases from an in-country MCP server and
needs a caseworker's approval to change one:
apiVersion: maqpna.com/v1alpha1
kind: Agent
metadata:
name: case-agent
namespace: maqpna-agents
spec:
image: registry.eu.internal:5000/agents/case-agent:1.0.0
tier: tier-2
model:
provider: vllm
endpoint: http://vllm.models.svc:8000/v1
name: mistral-small-3.2
tools:
- name: case-db
upstreamURL: https://case-db.tools.eu.internal/mcp
policyRef: case-agent-policy
maxSessionTTL: 2h
---
apiVersion: maqpna.com/v1alpha1
kind: ToolPolicy
metadata:
name: case-agent-policy
namespace: maqpna-agents
spec:
agents: ["case-agent"]
defaultAction: deny
rules:
- name: case-reads
servers: [case-db]
tools: ["get_*", "search_*"]
action: allow
- name: case-writes-need-approval
servers: [case-db]
tools: ["update_*", "close_*"]
action: require_approval
reason: Changes to a case need a caseworker.
Replace the registry, hosts and model with yours (maqpna-agents is the agent namespace the profile creates).
Check it works#
maqpna sovereignty check -f manifests/case-agent.yaml --policy manifests/sovereignty.yaml --tier manifests/
maqpna validate -f manifests/ --sovereignty manifests/sovereignty.yaml --strict
COMPLIANT Agent maqpna-agents/case-agent (manifests/case-agent.yaml)
3 file(s), 4 object(s): 0 error(s), 0 warning(s)
The same agent with its image on a public registry and a tool outside the EU is refused, with exit status 3:
NON-COMPLIANT Agent maqpna-agents/case-agent-us (us.yaml): 2 violation(s)
- [registry-not-allowed] spec.image: image "ghcr.io/acme/case-agent:1.0.0" (registry ghcr.io) is not from an allowed registry [registry.eu.internal:5000/]
- [egress-not-allowed] spec.tools[case-db].upstreamURL: host "api.example.com" is not in allowedEgressHosts/allowedEgressCIDRs
Run both in CI on every change (Policy as code with CI and GitOps).
Step 3: Create the keys you hold#
MAQPNA never generates or escrows the signing keys in this profile. Generate them on a trusted machine (or take them from your HSM or KMS), then store them as Secrets:
kubectl create namespace maqpna-system
# Identity signing key: signs every session token (Ed25519 PKCS#8)
maqpna keygen -out ./keys --name identity
kubectl -n maqpna-system create secret generic maqpna-identity-key \
--from-file=key.pem=./keys/identity.key --from-file=pub.pem=./keys/identity.pub
# Audit-checkpoint key: signs the ledger head every 60 s, and usage reports
maqpna keygen -out ./keys --name audit-signing
kubectl -n maqpna-system create secret generic maqpna-audit-signing \
--from-file=audit-signing.pem=./keys/audit-signing.key
maqpna keygen writes NAME.key and NAME.pub. Keep the private keys offline after this step, or back them up
with maqpna backup create --seal-to (Backup, restore and DR).
To keep the checkpoint key inside a KMS instead, leave out the maqpna-audit-signing Secret and set
audit.checkpointSigning.keyURI in step 4, for example kms://vault/transit/keys/maqpna-audit. Release images
sign with kms://aws, kms://gcp, kms://azure, kms://vault and kms://openbao keys. pkcs11: signing
needs custom images built with -tags pkcs11 and customKeyBackends: true. The identity key is always read from
a Secret; sync it from your HSM or KMS (see Cloud identity and keys).
sovereignty.keyCustody only records your custody model; it does not configure where keys come from.
Now the credentials for the in-country services:
kubectl -n maqpna-system create secret generic maqpna-state \
--from-literal=dsn='postgres://[email protected]:5432/maqpna?sslmode=verify-full'
kubectl -n maqpna-system create secret generic maqpna-audit-s3 \
--from-literal=access-key='<access-key>' --from-literal=secret-key='<secret-key>'
kubectl -n maqpna-system create secret generic maqpna-trustee-jwks --from-file=jwks.json
kubectl -n maqpna-system create secret generic maqpna-siem-tls --from-file=ca.crt
Check it works#
kubectl -n maqpna-system get secrets lists maqpna-identity-key, maqpna-audit-signing (unless you use a KMS key), maqpna-state,
maqpna-audit-s3, maqpna-trustee-jwks and maqpna-siem-tls.
Step 4: Write your values#
Keep the profile unchanged and put your overrides in my-values.yaml. Replace every CHANGE-ME the profile has.
A typical set:
image:
digests: # from step 1, one per image
maqpna-operator: sha256:<digest>
maqpna-gateway: sha256:<digest>
maqpna-identity: sha256:<digest>
maqpna-attest: sha256:<digest>
maqpna-attest-agent: sha256:<digest>
airgap:
imageRegistry: registry.eu.internal:5000/maqpna
sovereignty:
operator: "Example Operator GmbH"
allowedRegistries: ["registry.eu.internal:5000/"]
allowedEgressHosts: ["*.svc", "*.svc.cluster.local", "*.tools.eu.internal", "idp.eu.internal"]
allowedEgressCIDRs: ["10.20.0.0/16"]
attestation:
trusteeURL: https://kbs.attest.eu.internal:8080
referenceValues:
allowedMeasurements: ["<launch measurement, hex>"] # empty rejects every report
snpHostData: ["<Kata init-data digest, hex>"] # TDX: tdxMrConfigId
minSvn: 0
audit:
worm:
endpoint: https://minio.audit.eu.internal:9000
sinks:
- name: siem
type: syslog
address: tls://siem.tools.eu.internal:6514
format: rfc5424-json
tlsSecret: maqpna-siem-tls
identity:
trustDomain: agents.eu.internal
gateway:
config:
upstreams:
case-db: https://case-db.tools.eu.internal/mcp
web:
enabled: false # no public web fetch in this tutorial
adminAuth:
oidc:
issuer: https://idp.eu.internal/realms/maqpna
principalBinding:
trustedCreatorGroups: []
Every endpoint must match allowedEgressHosts or allowedEgressCIDRs: the gateway's residency-checking dialer
refuses any upstream, SIEM or WORM address outside them.
Check it works#
maqpna values validate --chart ./maqpna -f maqpna/values-sovereign-eu.yaml -f my-values.yaml --profile sovereign
It prints OK: values valid and exits 0. The validator applies the chart guards, the production rules and the
EU AI Act compliance profile the sovereign profile sets. A values file that brings back a generated identity key
and a 90-day WORM lock fails with exit status 3:
SEVERITY RULE PATH MESSAGE
error chart identity.replicas identity.replicas > 1 requires a shared key: set identity.key.mode to helm or existingSecret
error production identity.key.mode identity.key.mode=generate makes a new signing key on every restart (tokens stop verifying); use helm or existingSecret
error compliance audit.worm.objectLockDays eu-ai-act: WORM retention 90 days is below the 183-day floor
Step 5: Run the preflight checks#
maqpna preflight evaluates the values install would use and checks the
cluster. It creates nothing:
maqpna preflight --chart ./maqpna --profile sovereign-eu -f my-values.yaml --strict
It prints PASS, WARN and FAIL lines with a fix: line under each problem. Checks that matter here:
runtimeclass-<name> fails when no node carries the kata-qemu-snp (or kata-fc, runsc) handler,
image-registry-allowed fails when an image is outside sovereignty.allowedRegistries, and agent-sandbox
fails when the agent-sandbox CRDs are missing.
Check it works#
The command exits 0. Fix every FAIL and WARN first.
Step 6: Install#
maqpna install --chart ./maqpna --profile sovereign-eu -f my-values.yaml --wait --timeout 15m
install applies the CRDs, installs the release maqpna into maqpna-system and waits until every workload is
ready. With plain Helm or GitOps, use helm upgrade --install with the same two values files.
Check it works#
maqpna status
maqpna get sovereigntypolicy default
maqpna tier list
The policy shows jurisdiction EU, enforcement enforce and no violations. The tiers are tier-0 (gvisor,
egress deny-all), tier-1 (kata-fc) and tier-2 (kata-qemu-snp).
The profile turns off the static admin token, so sign in through your identity provider. Port-forward the gateway
and log in with a user in the maqpna-admins group:
maqpna port-forward gateway 8080:8080 &
maqpna login --gateway http://127.0.0.1:8080 --device
maqpna doctor -n maqpna-system --strict
doctor exits 3 when a posture or cluster check fails (Security hardening).
Step 7: Deploy the agent#
Apply the manifests you checked in step 2. maqpna apply runs the same offline validation first, then a
server-side apply:
maqpna apply -f manifests/case-agent.yaml
maqpna wait toolpolicy/case-agent-policy -n maqpna-agents --for condition=Ready
Check it works#
maqpna agent get case-agent -n maqpna-agents shows the agent, and maqpna get sovereigntypolicy default still
shows 0 violations. An agent that breaks the policy is refused at admission with the violation codes
maqpna sovereignty check printed.
Step 8: Run a session on tier-2#
The profile sets principalBinding.mode: requester: a session's user must be the Kubernetes user who creates it.
Pass your own Kubernetes username:
maqpna session start --agent case-agent -n maqpna-agents --user "<your-kubernetes-username>" \
--task "Summarise open case 4711" --wait --timeout 10m
A tier-2 session token is never written to a Secret. An init container in the confidential VM sends a hardware
report to maqpna-attest, which has your Trustee appraise it, checks your reference values, and only then mints
the token sealed to a key inside the VM. See Attestation flow.
Check it works#
maqpna session list -n maqpna-agents --agent case-agent
kubectl -n maqpna-system port-forward deploy/maqpna-attest 8082:8082 &
curl -s http://127.0.0.1:8082/metrics | grep -E 'maqpna_attest_(success|failure)_total'
The session reaches Running and maqpna_attest_success_total has increased. If the session fails, run
maqpna session describe <session> -n maqpna-agents. A condition with reason AttestationFailed means the
report did not match your reference values, often after a firmware or guest-image update. Update
attestation.referenceValues through your change process; see
Attestation failure runbook.
Step 9: Verify the evidence#
Fetch the ledger with its signed checkpoints and verify it offline. The JWKS the gateway publishes holds the public
half of the checkpoint key you created. You need the auditor or admin role:
maqpna audit fetch ledger --gateway http://127.0.0.1:8080 --out ledger.jsonl
maqpna audit verify ledger.jsonl --jwks ledger.jsonl.jwks.json
audit fetch ledger also writes ledger.jsonl.checkpoints.jws and ledger.jsonl.jwks.json. audit verify
recomputes the hash chain, checks every checkpoint signature, and exits 3 when a record was changed or removed.
Check it works#
audit verify prints OK and exits 0. maqpna audit sinks --gateway http://127.0.0.1:8080 checks that the WORM
bucket and SIEM receive the same records; it exits 3 when a sink lags or has errors.
What you built#
- An installation from the
sovereign-euprofile, with images only from your EU mirror, shared state in in-country PostgreSQL, three gateway replicas and audit that fails closed. - A sovereignty policy enforced at admission and checked offline, with
maqpna sovereignty checkandmaqpna validate --sovereignty. - An agent on tier-2 (confidential VM) whose session token is released only after hardware attestation.
- Identity and checkpoint keys you generated and hold, and an audit ledger you verified against your own key.
Clean up#
maqpna session delete <session> -n maqpna-agents --yes
kubectl delete -f manifests/case-agent.yaml
maqpna uninstall --wait # export the audit ledger first
The CRDs, custom resources, key Secrets and agent namespaces stay unless you add --purge. Keep your key
backups: the checkpoints can only be verified with the matching public keys.
Next steps#
- Sovereignty, confidential tiers and customer-held keys: every sovereignty field, attestation settings and key URIs.
- Production install with Helm: ingress, TLS, OIDC roles and the state backend in depth.
- Key rotation runbook: rotate the identity and checkpoint keys without downtime.
- Production checklist: what to confirm before go-live.
- DLP profiles: the
eu-strictprofile this installation uses.