MAQPNADocs

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-eu profile, 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 check and maqpna 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#