Skip to content

US cluster with EU OpenBao, Port and customer relay

Published release set

Use chart 1.0.359, operator v0.13.209, wrapper/init v0.2.207, and Linux host Port v0.4.133. Use released CLI v0.19.176 with this matched set. The introduction exporter refuses unpublished cohort pins before private file access or source credential use. Install chart 1.0.359. Do not use 1.0.357 or 1.0.358. This is a fresh-install procedure; in-place upgrades from older charts are untested.

The proven path is Linux host Port + customer relay + US cluster + EU store. This procedure covers one Kubernetes cluster with compute in the US. OpenBao and your Port run together on your EU Linux VM; a second customer-controlled EU VM runs the self-hosted strict Wire v2 beacon relay. Port and the cluster's broker connect outbound to that relay on TCP 443. The design keeps source storage in the EU and delivers approved values to the US application; it does not claim that consumed secret bytes never leave the EU. The cluster needs no inbound route to OpenBao. The EU management session needs the US Kubernetes API, local OpenBao and the approved release endpoints.

All live results on 31 October and 1 November 2026 came from a single-node cluster, with one operator replica on US GKE Standard COS 6.12.105+ with Calico, EU Linux VMs, Port v0.4.133, wrapper/init v0.2.207 and beacon v0.6.23. The initial dated lab walk followed the previous revision of this procedure on chart 1.0.359 with released CLI v0.19.173, using lab substitutions: a tmpfs token file instead of the root-token prompt and a static-token kubeconfig. The verification walk on 1 November 2026 followed the published guide with the same chart and CLI, Port v0.4.133, beacon v0.6.23, operator v0.13.209 and wrapper/init v0.2.207. It used real attached SSH terminals and the root-token prompt; --attach, step 6's identity re-read, the published verification blocks and the namespace-delete block all ran live. It still used a static tester-token kubeconfig and a harness to type the custody token into the real prompt. The longer session lab walk measured Port relaunch and deadline behavior on that same release set, again with attached SSH terminals and automated root-token entry; see step 5. The deadline and firewall lab walk followed the guide with CLI v0.19.176 and the same chart, Port and beacon. It measured the pre-expiry stop margin, fault cleanup, human-paced relaunch and the firewall blocks as written. A root helper typed the token at human pace; it was not typed by a person. These results remain limited to this lab. Earlier runs used pre-release CLI builds on charts 1.0.358 and 1.0.359. The tests used an IP-literal relay with a private CA and no SNI allowlist; the cluster reached it at a private VPC address, not over the public internet. A DNS-named or public-CA relay was not tested. The example below follows the measured private-CA form.

Check Run on Observed result and boundary
EU host access, US API and TokenReview 1.0.358 and 1.0.359, including the initial dated lab walk Verified TLS. TokenRequest API tokens authenticated with the exact SA/UID and audience; wrong audience and altered signature failed. Another SA authenticated as itself.
Application delivery 1.0.358 and 1.0.359, including the initial dated lab walk In-cluster Service checks matched the synthetic value; source audit recorded the exact approved path/version. The initial dated lab walk ran the fixture below.
Wrong-SA and wrong-path authorization 1.0.358 and the initial dated lab walk on 1.0.359 Port refused the wrong SA; webhook refused the wrong path. The initial dated lab walk recorded no extra source read.
Default application resources 1.0.359 No application resources: requests 50m/32Mi, no injected limit, Ready first start. The initial dated lab walk's serving Pod had zero restarts over about 50 minutes; one new Pod recovering from source seal had one container restart. Explicit budgets also passed. 1.0.358's default 64Mi cap caused OOM.
Static-KV rotation Including the 1 November 2026 verification walk on 1.0.359 Explicit target rotate delivered the new value with matching audit. After v3 was written, 97 probes over 96.7 seconds still returned v2, same Pod UID, zero restarts and no secret audit reads; explicit rotation then delivered v3.
Relay stop and source seal The initial and 1 November 2026 verification walks on 1.0.359 Serving app retained its value; new Pods recovered after restoration. The 1 November 2026 verification walk measured a 30-second relay drain and source-seal recovery without a container restart. See step 8.
Port stop then fresh launch The verification, longer session, and deadline and firewall walks on 1.0.359 The deadline and firewall lab walk measured 33.47 s with a 20 s operator pause, 49.54 s with a 40 s pause, and 9.06 s with the launch line already waiting. No Pod CREATE was refused. See step 5.
Default NetworkPolicies and store firewall The verification and deadline and firewall walks on 1.0.359 Protected-Pod egress probes blocked the tested destinations, with DNS, API, node :19099 and operator :8199 still reachable. The deadline and firewall lab walk ran the host chain and CA-verified check as written: three timeouts, 15 DROP packets, relay HTTP 200 and protected delivery. Run the store firewall check after step 7, before step 8.
Approval deadline and token revocation The deadline and firewall lab walk on 1.0.359, CLI v0.19.176 Port now starts stopping about 4 s before expiry; all eight natural-deadline revocations succeeded. Store faults still left tokens, including a stop with the same session text as a clean deadline stop. Reconcile after every launch ends; see step 5.
Operator replacement 1.0.358 only Fresh private introduction on an existing Port session passed. HA was not tested.
Uninstall and cleanup 1.0.358 and 1.0.359, including the initial dated lab walk Hooks/owned-resource cleanup passed; the initial dated lab walk reconciled the expired host launch to remaining_grants=0. The retained source still had one AppRole role and four policies; cluster uninstall is not complete source-grant retirement.

The 1 November 2026 verification walk ran the published CLI/beacon checksum and chart verification blocks with OpenSSL, and the published operator-image block with Cosign v3.1.4 on both the source VM and a clean management-host environment. All matched CloudTaser chart images passed key-signature verification; negative controls failed. This establishes key signatures, not transparency-log inclusion. Third-party helper images have the narrower evidence described in supply-chain verification.

Release limits

Managed S3 proxy and Volume Shield are not supported on this path. DB proxy has not been shown to work on this path; no store-backed DB serving test passed. Use one wrapped container per Pod and explicitly set cloudtaser.io/containers; it defaults to the first container. Two wrapped consumers fail closed by source inspection, observed with the S3 sidecar shape (two application containers were not tested). Linux 7.0 is unsupported. Stock GCE ubuntu-2404-lts-amd64 booted 7.0.0-1014-gcp and the agent crash-looped. Check uname -r on every node: an LTS distribution name does not pin its kernel. GKE COS 6.12.105+ passed delivery. Ubuntu 24.04 linux-image-gcp-lts-24.04, 6.8.0-1071-gcp, with bpf in the lsm= boot line passed admission/startup only, without a store. Broker CA rotation on host Port has no supported runbook yet. A rehearsal used hand-built enrollment and material; it does not establish a supported customer procedure. Pod deletion does not immediately revoke its issued store tokens: they can outlive the deleted Pod until TTL, up to 15 minutes. The test measured 13 minutes 48 seconds of remaining token life, not a post-deletion secret read.

Hosted customer/pilot enrollment is not currently available. Use your own Port and relay. Other Kubernetes providers/CNIs, managed Kubernetes Port, HA, unattended renewal, signing-key rotation and a new authority generation on an already-used source are not yet verified by this test. Use a fresh dedicated source for this procedure; do not improvise generation changes on a used source.

1. Arrange the release, hosts and self-hosted relay

Use a fresh systemd Linux host with no disk-backed swap, a qualified Kubernetes cluster (the measured path uses GKE Standard COS with Calico), Helm, kubectl, Bash, curl, jq, Python 3, OpenSSL, base64, tar, sha256sum, IPv4 iptables, awk, Cosign (v3.1.4 was tested) and the release CLI (also on the relay host). Keep eBPF, strict enforcement, signature verification and network policy enabled. Review the exact node/kernel and public site-network inputs for that release; this guide does not turn a default cluster into a qualified protected substrate.

First follow fetch and verify the matched host release: run the CLI block on both EU hosts, the chart block on the source/management host, and the beacon block on the relay host. Those blocks give the release URLs, pinned-key checks, signature/digest checks and installation commands. The Port installer in step 2 verifies its own pinned download. Use that reviewed, signed and matched CLI/Port/operator/wrapper cohort, its verified chart archive, its extracted values-production.yaml, the appropriate platform overlay and a reviewed public site-values file. Step 7 supplies a digest-pinned synthetic application manifest. For offline docs, set DOCS_ARTIFACT_DIR on each EU host to the local reviewed built-site directory; the download blocks then copy its public assets. Do not mix independently selected component versions. The published chart bundles the operator and wrapper/init versions listed above; no candidate wrapper override is required. Verify the signed chart and matched image digests/signatures; see How to verify an image. Preserve signature verification. Skip charts 1.0.357 and 1.0.358. Port accepted the CLI's authority catalog in the live test. Use released CLI v0.19.176 for this procedure.

Export only public coordinates:

export KUBECONFIG_FILE=/absolute/path/us-cluster.kubeconfig
export KUBECONFIG="$KUBECONFIG_FILE"
export CORE_CHART_ARCHIVE="/root/cloudtaser-release/cloudtaser-1.0.359.tgz"
export PRODUCTION_VALUES=/root/cloudtaser-release/matched-chart/values-production.yaml
export PLATFORM_VALUES=/root/cloudtaser-release/matched-chart/values-gke-cos.yaml
export SITE_VALUES=/absolute/path/reviewed-site.yaml
export BEACON_ADDRESS=198.51.100.20:443
export RELAY_SOURCE_IP=192.0.2.20
export HOST_APPROVAL_FILE=/root/cloudtaser-release/host-approval.json
export BEACON_TENANT_ID=01234567-89ab-cdef
export SOURCE_LISTEN_ADDRESS=192.0.2.10:8200
export SOURCE_ORIGIN="https://$SOURCE_LISTEN_ADDRESS"
export SOURCE_CA=/etc/cloudtaser/source-ca.pem
export BEACON_CA=/etc/cloudtaser/relay-ca.pem
export MARKER_V1_SHA256=$(printf '%s' preview-v1 | sha256sum | cut -d ' ' -f 1)
export MARKER_V2_SHA256=$(printf '%s' preview-v2 | sha256sum | cut -d ' ' -f 1)

Replace those example coordinates. Confirm the context and kube-system UID against the intended US cluster inventory; a kubeconfig filename is not proof of cluster location. Prove authenticated management access to both EU hosts before bootstrap. Other substrates need separate qualification; replace 198.51.100.20 with your relay IP and supply its private CA PEM. Replace 192.0.2.10 with the source VM's actual private interface address. Set RELAY_SOURCE_IP to the relay's private-network source IPv4 address as seen by the store, including any source NAT. This is separate from BEACON_ADDRESS, which is the address Port and the cluster dial; do not derive one from the other. The relay enrollment helper must reach this listener over your private EU network; the US cluster does not need access. Permit only the approved management/relay peers and the local Port; an example after step 7 restricts the host listener. The source TLS certificate must cover that address; its private key stays in OpenBao, not in the Port account. Use this same verified SOURCE_ORIGIN in each source and relay management session. Make the public source CA readable by Port (0644 with administrator-owned searchable parents). Copy the public source CA PEM to the relay host at its SOURCE_CA path; copy the public relay CA PEM to the source/management host at its BEACON_CA path. Use your authenticated host-transfer channel and compare each copy's sha256sum with the issuing administrator's approved digest. Both sessions must use the corresponding verified CA; do not copy either CA's private key.

Hosted enrollment is not currently available, including for customer pilots. The CloudTaser-hosted fleet is reserved for its existing demo boundary. Do not wait for a hosted tenant, use its demo identity/key or point this procedure at beacon.cloudtaser.io. There is no customer enrollment API or pilot handoff.

Prepare your own EU relay with a fixed IP, a certificate with that IP SAN, verified TLS, qualified signed beacon binary and a tenant-key consumer under your control. It needs reachable TCP 443 from Port and the cluster; OpenBao stays private. Choose your own canonical public tenant ID (the example above must be replaced) and bind the same ID to your source and relay. Your relay/source administrators own the private issuance and restart custody; no CloudTaser-hosted action is required. See self-hosted relay setup.

Private issuance happens after the source KV mount exists (step 3). The CLI's legacy single-tenant beacon install/ctb1 workflow does not create this registered Wire v2 setup, and beacon install --tenant-mode=multi is not a supported shortcut. Arrange your source administrator's complete registered v2 operator introduction handoff as well (step 6). These are your administrative duties, not provider onboarding or self-service commands invented by this guide.

Use a private, unrecorded root Bash management session for bootstrap/launch. Re-establish the public variables and helpers in each management session; step 6 re-reads the cluster UID in the second session. Disable tracing; obtain your own initial root token from bootstrap custody. This helper prompts privately and emits only to a pipe; it is a local Bash function, not a CloudTaser subcommand:

set +x
set -euo pipefail
umask 077
customer-root-token() {
  local token
  IFS= read -r -s -p 'Initial OpenBao root token: ' token </dev/tty || return
  printf '\n' >/dev/tty
  printf '%s\n' "$token"
  unset token
}

Interactive shell failures

set -euo pipefail remains active in the interactive shell. A failed command can close that root shell and lose its exports, helper and umask; the 1 November 2026 verification walk encountered this after an input error and an API timeout during uninstall. Reopen a private root Bash session, restore the exports/helper, umask 077 and working directory, inspect what completed, then retry the failed command. Do not replay provisioning or preparation blocks blindly. After launch, leave session 1 attached to Port and run subsequent work only in session 2.

Keep umask 077. Before source staging, the matched CLI sets and verifies root-owned /run/cloudtaser to 0755 for service-user traversal, while its service-owned host-launches child stays 0700 on tmpfs. It repairs an existing safe root-owned 0700 parent from an earlier launch. Unsafe ownership, symlinks or writable parents fail before the launch is spent. Do not use a shell umask change to recover a spent approval.

Initial provision/activation require a root-only caller (policies=["root"], no identity policies), and a quiet root-only token census with no legacy issuer policies/auth mounts. Do not mint scoped admin, app or Port tokens beforehand. A refusal requires source maintenance, not a census bypass. General source configure creates legacy policies and is not used here; the tenant-only mode below is permitted.

2. Install, initialize and unseal EU OpenBao; install the Port binary

Render public installers on the EU host:

cloudtaser-cli source install --type linux --managed-authority \
  --secretstore-address "$SOURCE_LISTEN_ADDRESS" \
  --tls-cert-file /etc/openbao/tls/server.crt \
  --tls-key-file /etc/openbao/tls/server.key > install-openbao.sh
cloudtaser-cli source install port --type linux --dry-run > install-port.sh
bash -n install-openbao.sh
bash -n install-port.sh

Review both scripts before execution. Require the OpenBao Raft listener's cluster_address and advertised cluster_addr to use 8202, leaving 8201 for Port health. A script generated without that correction needs reviewed maintenance before execution. The source installer selects its pinned OpenBao, initializes 5 shares/threshold 3, unseals through stdin, enables audit, AppRole and the application/authority KV-v2 mounts. It creates no legacy CloudTaser Kubernetes login configuration. Its stdout contains the initial root token and unseal shares: use protected bootstrap custody, never a captured terminal, CI log or shared output file. Do not rerun initialization on a live store; retain unseal custody and the data volume.

# Execute only after script review, in the private bootstrap session.
BAO_CACERT="$SOURCE_CA" bash install-openbao.sh
bash install-port.sh

The Port installer verifies signed metadata and the binary digest, installs a root-owned executable plus the non-root cloudtaser-port account, and starts no service. Installing a binary is not admission or a store login.

3. Enroll the beacon tenant at the source and relay

Customer relay/source-administrator setup, after the cloudtaser KV-v2 mount exists. Your authorized supervisor runs one fresh tenant issuance, delivering the same record through two distinct private pipes: one to the relay's private runtime-map consumer, the other to the source consumer below (with the same private SOURCE_ORIGIN set on the relay host). A third independent read-only pipe, FD K (at least 3), supplies the initial root token to the source:

cloudtaser-cli source configure --beacon-tenant-stdin \
  --beacon-tenant-id CUSTOMER_TENANT_ID \
  --secretstore-address "$SOURCE_ORIGIN" \
  --secretstore-ca-file /etc/cloudtaser/source-ca.pem --admin-token-fd K

This is the consumer's exact invocation, not a standalone command to paste without its inherited pipes. Stdin carries the tenant record, not the root credential. The source creates its independent bootstrap salt; paired per-cluster salts remain separate. No secret record may be printed or saved to disk, Helm values or Kubernetes objects. Both consumers must acknowledge persistence and your relay administrator must confirm the tenant is enabled in your relay runtime map. On an uncertain/partial result, stop and reconcile with both owners; do not issue again, overwrite a tenant or rotate paired salts as a retry.

Use the standalone strict relay procedure: the shipped beacon-relay-enroll.py supervisor provides these private pipes and the bounded record-to-map consumer; its strict systemd unit uses LoadCredential so the non-root service can read the root-only map. Follow that path rather than inventing a consumer, running a second bare binary or modifying the legacy shared-key installer's environment. Require its issuer/source/relay receipts before activation. Your administrators retain the same-key restart custody.

Keep only public tenant ID, source KV version, relay version and success receipts. See Beacon enrollment.

4. Approve, provision and activate the exact cluster/workload authority

Before provisioning, place the reviewed US target kubeconfig and any required CA/credential files on the EU source VM through your private administrator channel. Set KUBECONFIG_FILE there to its absolute local path and protect it from other users. Review any kubeconfig exec plugin: this root ceremony can execute it on the source VM. Do this before the provision step, rather than copying the kubeconfig during later cluster installation.

From that source VM, the selected credential must already have get on namespace kube-system and the non-resource URLs /.well-known/openid-configuration and /openid/v1/jwks. The chart has not been installed yet; its later RBAC cannot supply these bootstrap permissions. Keep verified API TLS enabled. The CLI checks the namespace UID, preserves the discovery issuer exactly and fetches keys at /openid/v1/jwks on that same authenticated API origin, even if discovery advertises a container.googleapis.com URL. Provision writes and reads back the public trust document before publishing the staged authority; activation refuses missing or foreign trust. Namespace, discovery, key and validation failures stop provisioning.

The installing administrator also needs namespace/SA and preflight RBAC creation, Helm's cluster-scoped installation rights, TokenRequests (create on serviceaccounts/token) and SA reads for the gate, and activation access to Pods, Pod logs, events, Services, ConfigMaps, pods/portforward and Deployments in cloudtaser-system. Application checks need Deployments, Services and policies in payments, plus temporary client Pods in payments-checks. Do not give these installation permissions to the reviewer-only SA.

On the EU source VM, create the namespaces and SAs before the gate below. Use fresh dedicated names; inspect existing resources rather than adopting them.

kubectl --kubeconfig "$KUBECONFIG_FILE" create namespace payments
kubectl --kubeconfig "$KUBECONFIG_FILE" label namespace payments cloudtaser.io/inject=true
kubectl --kubeconfig "$KUBECONFIG_FILE" -n payments create serviceaccount reader
kubectl --kubeconfig "$KUBECONFIG_FILE" -n payments create serviceaccount other-reader
kubectl --kubeconfig "$KUBECONFIG_FILE" create namespace payments-checks
kubectl --kubeconfig "$KUBECONFIG_FILE" -n payments-checks create serviceaccount token-reviewer
kubectl --kubeconfig "$KUBECONFIG_FILE" create clusterrole payments-token-reviewer \
  --verb=create --resource=tokenreviews.authentication.k8s.io
kubectl --kubeconfig "$KUBECONFIG_FILE" create clusterrolebinding payments-token-reviewer \
  --clusterrole=payments-token-reviewer --serviceaccount=payments-checks:token-reviewer

On that EU source VM, read the actual target identity and prepare a public approval for namespace payments, ServiceAccount reader, and one exact KV path:

CLUSTER_UID=$(kubectl --kubeconfig "$KUBECONFIG_FILE" \
  get namespace kube-system -o jsonpath='{.metadata.uid}')
cloudtaser-cli source authority prepare --instance eu --generation g1 \
  --fingerprint "$CLUSTER_UID" --namespace payments --service-account reader \
  --secret-path secret/data/payments/credentials > authority-approval.json
cloudtaser-cli source authority render --file authority-approval.json > authority.json

Before provisioning, run this gate from the actual EU source/Port host against the confirmed public US API endpoint in the kubeconfig. A private endpoint or TLS handshake alone is insufficient. Download and review the published TokenReview gate, then run:

if test -n "${DOCS_ARTIFACT_DIR:-}"; then
  cp "$DOCS_ARTIFACT_DIR/assets/scripts/tokenreview-gate.py" tokenreview-gate.py
else
  curl --fail --silent --show-error --proto '=https' \
    https://docs.cloudtaser.io/assets/scripts/tokenreview-gate.py -o tokenreview-gate.py
fi
python3 tokenreview-gate.py --kubeconfig "$KUBECONFIG_FILE"

The helper executes separate kubectl create token TokenRequests (10-minute lifetime), then HTTPS TokenReviews using only the reviewer SA. It checks payments:reader, its UID and cloudtaser.io/secret-envelope; wrong audience and altered signature must fail. other-reader must authenticate as itself, with a different UID: TokenReview is authentication, not Port authorization. The reviewer must also be denied access to kube-system Secrets. Tokens remain in process memory, are never printed and are not concatenated: kubectl create token has no trailing newline. These are TokenRequest API tokens with the same signer as projected tokens, matching the live preflight; application projection/delivery is checked later. Stop on any failed assertion.

Review both public documents. Then provision and activate using separate private root input for each operation:

customer-root-token | cloudtaser-cli source authority provision \
  --file authority-approval.json --beacon-address "$BEACON_ADDRESS" \
  --kubeconfig "$KUBECONFIG_FILE" \
  --secretstore-address "$SOURCE_ORIGIN" \
  --secretstore-ca-file "$SOURCE_CA" --secretstore-token-file -
customer-root-token | cloudtaser-cli source authority activate \
  --file authority-approval.json --beacon-address "$BEACON_ADDRESS" \
  --expected-active-version 0 --secretstore-address "$SOURCE_ORIGIN" \
  --secretstore-ca-file "$SOURCE_CA" --secretstore-token-file -

Version zero is only the first activation. Later changes require maintenance with the actual active KV version. A grant creates permission, not application data. After activation, create a synthetic test value using your source administrator session (the command-scoped Bao token is kept out of argv):

BAO_ADDR="$SOURCE_ORIGIN" BAO_CACERT="$SOURCE_CA" \
  BAO_TOKEN="$(customer-root-token)" \
  bao kv put secret/payments/credentials marker=preview-v1

Do not use real credentials as demonstration values or enable shell tracing.

Signing-key rotation and cluster binding

One managed source supports one registered cluster and issuer. Existing unbound legacy trust, a rebuilt cluster with a new UID, or an issuer change is refused. There is no CLI trust-migration command; use an isolated source or arrange reviewed source-administrator migration. Do not use target connect to replace managed trust.

Coordinate signing-key overlap with the cluster administrator. While the API serves both old and new public keys, refresh with the current active approval and the reviewed kubeconfig already on the source VM:

customer-root-token | cloudtaser-cli source authority refresh-jwks \
  --file authority-approval.json --kubeconfig "$KUBECONFIG_FILE" \
  --secretstore-address "$SOURCE_ORIGIN" \
  --secretstore-ca-file "$SOURCE_CA" --secretstore-token-file -

Refresh uses root maintenance, checks the active catalog and registered cluster, and updates only public JWT trust. It does not reload a running Port. Stop the old admitted process and restart/re-admit through the authorized host lifecycle before tokens start using the new key; a finite launch needs a fresh approval. Refresh and restart again after removing old keys. Restarting with stale stored keys cannot repair rotation. After root retirement, use administrator recovery before this maintenance. Provider signing-key rotation and this refresh procedure were not exercised in the live delivery test. This is separate from broker CA rotation, which has no supported host Port runbook yet.

Host DNS prerequisite before launch

Port deliberately isolates the GCE metadata endpoint. Stock GCE VMs can also use that endpoint as their host DNS resolver, so launch refuses with port metadata isolation blocks the host DNS resolver; configure a local forwarding stub before host launch. Keep metadata isolation enabled. Before launch, have the host administrator inspect resolvectl status, /etc/resolv.conf and actual relay/source DNS.

For the stock GCE systemd-resolved case, only after confirming resolved is active and its loopback stub exists, select that stub:

systemctl is-active systemd-resolved
resolvectl status
test -f /run/systemd/resolve/stub-resolv.conf
ln -sfn /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
# For a DNS-named relay only (that variation has not been tested):
# resolvectl query "${BEACON_ADDRESS%:*}"

Run each check successfully before the change. This is specific to hosts running systemd-resolved; other resolvers need their supported forwarding setup. Do not apply a blind symlink or permit Port to reach metadata as a workaround. A host DNS lookup is only a preflight: actual admitted Port registration still needs a registration/delivery check after launch. IP-literal registration passed in the live tests; DNS-named relay registration was not exercised.

5. Admit Port and log in to OpenBao

Prepare the public, finite host approval on that same EU host. Choose a new absolute HOST_APPROVAL_FILE path for every launch, including retries and fresh launches after a stop. Shell redirection in this block overwrites an existing file; retain each original approval for its own later reconciliation. Do not rerun this block with a previous launch's filename:

cloudtaser-cli source host-launch prepare --authority-file authority-approval.json \
  --secretstore-address "$SOURCE_ORIGIN" \
  --secretstore-ca-file "$SOURCE_CA" --beacon-address "$BEACON_ADDRESS" \
  --beacon-tenant-id "$BEACON_TENANT_ID" --beacon-ca-file "$BEACON_CA" \
  --lifetime 1h > "$HOST_APPROVAL_FILE"

Review machine/boot ID, service UID/GID, executable version/SHA, CA digest, authority digest and expiry. Do not supply a component override. The launcher independently verifies these and the active source graph. prepare --lifetime accepts 2 minutes to 24 hours; the deadline and firewall lab walk refused 1m and 119s, and accepted 2m. Launch requires at least 64 seconds remaining: one minute of runtime plus a four-second pre-expiry stop margin. Review time consumes that budget; prepare a fresh approval if too little remains.

The staging clock check compares the CLI host clock with OpenBao's health response and refuses a difference over two seconds before spending the approval. In this guide, CLI and OpenBao run on the same host and read one clock; this does not detect that host's clock being wrong against an external clock. The deadline and firewall lab walk's simulated health-time shifts of +5 s and -5 s were refused, but the attached session showed only host worker public stage failed, not the clock reason. A real +5 s host clock step passed staging. Check host time synchronization independently; the generic failure text does not identify skew.

Do not run accessor scans while a launch is starting, including reconcile or revoke for another approval. The deadline and firewall lab walk reproduced startup failures when accessor lookup raced token creation, including with the supported reconcile command. A failed start spends the approval, and new Pods wait for the next launch (up to 207 seconds in the lab). The tight-loop test was much faster than a person; the rate at human pace is unknown, but a manually started scan can still overlap startup. Finish cleanup before starting the next launch, or wait until startup has completed.

rc=0
cloudtaser-cli source host-launch --approval-file "$HOST_APPROVAL_FILE" \
  --admin-token-fd 3 --timeout 65m 3< <(customer-root-token) || rc=$?
echo "launch rc=$rc"

Keep that root Bash session attached; continue cluster setup in a second private session. To stop it deliberately, press Ctrl-C in the attached launch session to signal the foreground process group; wait for port_stopped. The 1 November 2026 verification walk tested terminal Ctrl-C in a real attached SSH terminal: it returned rc=1 with context canceled, and the shell stayed open. Earlier parent-only SIGINT also returned that combination. After your deliberate interrupt, that combination is the observed clean stop. In a shell with set -e, the block above captures that nonzero status so the shell remains open. Inspect a launch failure before continuing; rc=1 alone is not proof of a clean stop. Do not put sudo around a command that already inherits a private FD. The supervisor stages the finite AppRole admission and passes a wrapped secret through a private pipe; Port unwraps/logs in to local OpenBao and dials the registered beacon. There is no static VAULT_TOKEN service unit. A public approval or an established TCP socket alone is not proof of secret delivery.

Use SSH keepalives for this attached session (for example ssh -o ServerAliveInterval=15 -o ServerAliveCountMax=3 SOURCE_HOST). The example is a supervised one-hour launch, not an unattended production daemon. Port now starts stopping about four seconds before the approval deadline; --timeout 65m never applies to this one-hour approval. The one-hour clock starts at host-launch prepare, not at launch; time spent reviewing or waiting consumes that hour. A deadline stop returned rc=1 with host admission ended; retained service exited and port_stopped, without context canceled in the 1 November 2026 verification walk. The longer session lab walk also ran --lifetime 70m with --timeout 75m (CLI v0.19.173; not repeated on v0.19.176): it passed both 60 minutes of approval age and 60 minutes of process age and kept delivering. No lifetime over 70 minutes was run. Use one hour. A 70-minute run is a measurement, not a recommendation.

Overlap is not a handover. In the longer session lab walk (CLI v0.19.173; not repeated on v0.19.176), launching while Port was live was refused in about two seconds with host managed service exited unsuccessfully. It spent the second approval and created its policy and AppRole, but no claim, SecretID or token. Retrying that approval failed with host worker public stage failed. The live Port's PID, relay session, operator and delivery were unchanged. Retain the failed approval and reconcile it after expiry as below; both overlap attempts reached remaining_grants=0.

The tested relaunch sequence is:

  1. Prepare the next approval ahead of time in a second private session on the source VM. Re-establish step 1's exports/helper and work in /root/cloudtaser-release. After re-establishing exports, run this block to select a unique absolute approval path and refuse to overwrite an existing file, then review the approval. Retain the current launch's original approval separately. Preparation starts the next approval's lifetime.

    export HOST_APPROVAL_FILE="$PWD/host-approval-$(date -u +%Y%m%dT%H%M%SZ).json"
    set -o noclobber
    cloudtaser-cli source host-launch prepare --authority-file authority-approval.json \
      --secretstore-address "$SOURCE_ORIGIN" \
      --secretstore-ca-file "$SOURCE_CA" --beacon-address "$BEACON_ADDRESS" \
      --beacon-tenant-id "$BEACON_TENANT_ID" --beacon-ca-file "$BEACON_CA" \
      --lifetime 1h > "$HOST_APPROVAL_FILE"
    set +o noclobber
    
  2. Stop the current Port with Ctrl-C well before its deadline in its attached launch session; wait for port_stopped and inspect the exit as described above.

  3. Wait for the old unit to unload, then launch at once in the session holding the next approval's path. A launch while the unit is still loaded can be refused and spend the approval. Supply the root token again. This session then becomes the attached one and takes no further work.

    until test "$(systemctl show -p LoadState --value cloudtaser-port.service)" = not-found; do sleep 0.2; done
    rc=0
    cloudtaser-cli source host-launch --approval-file "$HOST_APPROVAL_FILE" \
      --admin-token-fd 3 --timeout 65m 3< <(customer-root-token) || rc=$?
    echo "launch rc=$rc"
    

Every relaunch requires the root token and an attached session. The deadline and firewall lab walk used a root helper to type the token at human pace: 5.97 seconds from prompt to Enter, including a three-second reaction pause. With a 20-second pause after stopping Port, delivery was down 33.47 seconds; with a 40-second pause, 49.54 seconds. With the launch line already waiting in the LoadState loop, the gap was 9.06 seconds. No Pod CREATE was refused. Pods created in the two pause windows became Ready 15–35 seconds and 20–64 seconds after creation, up to 22 seconds after mTLS returned. Across the deadline and firewall lab walk, 104 of 104 canaries became Ready, with zero denied and one container restart; these are lab measurements, not a relaunch-time guarantee. The longer session lab walk's eight back-to-back relaunches measured 9.04–9.12 seconds with automated entry, including seven seconds of relay resumption grace. That walk had 4 of 125 canaries restart, three with secret envelope request denied at quiet times; the cause remains unestablished. Existing applications served throughout the measured relaunch gaps.

Reconcile after every launch ends; the session text does not prove token revocation. CLI v0.19.176 reserves a four-second stop margin and a two-second stop timeout. In the deadline and firewall lab walk, all eight natural-deadline stops revoked successfully, with revoke-self starting 4.07–4.82 seconds before expiry and taking 0.24–0.65 seconds. This improves the longer session lab walk's deadline race, but a store fault can still leave the login, enrollment and Pod tokens alive. The enrollment token can mint Pod tokens, and Pod tokens can read the secret; without revoke, surviving tokens last until expiry, up to about an hour in these runs. Reconcile does not revoke them.

In the deadline and firewall lab walk's forced TCP-reset fault, port_token_revocation_unacknowledged appeared. With silently dropped store packets, Port was SIGKILLed after its stop timeout, that warning was absent, and the session said host admission ended; retained service exited — the same text as a clean deadline stop. Both cases left tokens. Reconcile printed remaining_launch_tokens=1 remaining_grants=0 and exited non-zero; offline revoke then removed 10 of 10 and 6 of 6 tokens respectively, with the next reconcile returning zero. remaining_launch_tokens counts directly selected login tokens, not every live token: the count of 1 coexisted with 10 live tokens. remaining_grants=0 alone does not revoke or prove the absence of tokens. Use the cleanup blocks below, rather than relying on the stop message or the presence of a warning.

With no Port, new Pods are admitted but stay sealed, including Pods created by a rollout, target rotate, reschedule or node drain. In the longer session lab walk's 107-second gap, three new Pods recovered without recreation, up to 36 seconds after mTLS returned, because of retry backoff. The 1 November 2026 verification walk's existing synthetic app served through a 19-minute gap with zero failed probes; the operator exhausted 30 init attempts, restarted its loop without a container restart or RAM loss, and recovered after a fresh launch without operator reintroduction.

Disconnect, deadline, restart or reboot requires a fresh host approval and launch. A detached shell, nohup, & or disown in the same management session does not establish an approved persistent supervisor. SSH/session loss still needs recovery through a fresh attached launch or the separately qualified autonomous profile. SSH loss leaves downstream tokens alive until TTL; they can block later provisioning. The successful pre-deadline Ctrl-C revocations do not establish cleanup after an unclean loss. Failed/uncertain launch can consume its launch ID and leave its exact AppRole/policy behind; a clean exit also leaves them. Retain the original public approval. After its expires_at, in a private administrator session, re-establish step 1's exports/helper and run from /root/cloudtaser-release. Then explicitly set HOST_APPROVAL_FILE to the absolute path of that launch's retained original approval, overriding step 1's example or any later launch's path. Reconcile each launch separately with its own file; an absolute path avoids ambiguity from the current directory.

rc=0
customer-root-token | cloudtaser-cli source host-launch reconcile \
  --approval-file "$HOST_APPROVAL_FILE" \
  --secretstore-ca-file /etc/cloudtaser/source-ca.pem --secretstore-token-file - || rc=$?
echo "reconcile rc=$rc"

The command verifies the immutable source reservation, removes only that expired launch's policy and AppRole, and reports remaining_grants=0 plus remaining_launch_tokens=N. It exits non-zero when N > 0; || rc=$? keeps that result from closing the root shell under set -e. Inspect any non-zero result: an incomplete scan can report remaining_launch_tokens=unknown. Retry incomplete cleanup. Reconcile does not revoke surviving tokens; remaining_grants=0 proves only AppRole/policy cleanup.

If tokens remain, restore store access, stop the old launch and all its Ports, and ensure they cannot auto-restart. With that launch's retained original HOST_APPROVAL_FILE and independent administrator credentials, run:

rc=0
customer-root-token | cloudtaser-cli source host-launch revoke --confirm-offline \
  --approval-file "$HOST_APPROVAL_FILE" \
  --secretstore-ca-file /etc/cloudtaser/source-ca.pem --secretstore-token-file - || rc=$?
echo "revoke rc=$rc"

Revoke is safe to retry after partial failure and also works before approval expiry. The deadline and firewall lab walk retired four unexpired, cleanly stopped launches (revoked=0 remaining_launch_tokens=0). Revoking surviving tokens was observed only after expiry. After expiry, rerun reconcile and require rc=0, remaining_launch_tokens=0 and remaining_grants=0. Before expiry, inspect the revoke receipt; reconcile still requires expiry. Accessor revocation cascades to non-orphan children, but the receipt counts directly selected login tokens and reports descendants=unverified; zero is not proof that independently issued or orphan tokens are gone. Complete the CLI's offline cluster revoke-tokens scan for each fingerprint in that launch's catalog with affected issuers offline. Both commands retain spent source/host markers; never reuse the launch ID. Prepare a fresh approval for the next launch.

port_stderr_redacted deliberately hides raw Port stderr; retain public process/exit/endpoint evidence without printing credentials. Existing stores generated before the port-clash fix need reviewed maintenance to move both listener cluster_address and advertised cluster_addr to 8202, restart and unseal; changing only the advertised URL is insufficient.

Plan the pre-deadline relaunch and delivery interruption before launch; unattended renewal remains unverified. Do not revoke the bootstrap root mid-session or create an improvised long-lived Port credential.

6. Install the production cluster profile and introduce it through the beacon

The cluster release and operator run in namespace cloudtaser-system. In the second private session on the source VM, re-establish step 1's public exports and customer-root-token helper. Work in /root/cloudtaser-release for steps 6–9: external-port.yaml, cluster-public-render.yaml and protected-app.yaml are relative to it. Then re-read the live cluster identity before any export or Helm command:

cd /root/cloudtaser-release
export KUBECONFIG="$KUBECONFIG_FILE"
CLUSTER_UID=$(kubectl --kubeconfig "$KUBECONFIG_FILE" \
  get namespace kube-system -o jsonpath='{.metadata.uid}')
test -n "$CLUSTER_UID"

Compare it with the intended cluster inventory and step 4's approved identity. Keep the attached Port parent running in the first session.

Private source-administrator handoff: use the matched CLI's source authority export-operator-introduction with --instance eu, --fingerprint "$CLUSTER_UID", --kubeconfig "$KUBECONFIG_FILE", --secretstore-address "$SOURCE_ORIGIN", --secretstore-ca-file /etc/cloudtaser/source-ca.pem, --secretstore-token-file - and --output /run/cloudtaser-management/operator-introduction.json. Pipe a fresh private customer-root-token input to this command, as in the other source administration steps. Use the exact active authority instance if it differs from eu. The exporter authenticates to the source, verifies the live kube-system UID and registered authority/pairing, then atomically writes the complete six-field v2 introduction, mode 0600, without overwrite. It prints no secret and makes no source writes. Arrange this export before cluster activation. A wrong root-token input can misleadingly report managed source graph/registration is not active and verified; provision and activate the reviewed authority first. Check the private token input and existing authority state before retrying; do not rerun provisioning merely because of that diagnostic.

The file contains the tenant-wide beacon HMAC key and per-cluster registered salt, plus public version/mode/tenant/authority metadata. It has no time-based expiry or one-use limit: the target activate-beacon-auth command compares every field with the active source authority and pairing before injection. This client-side check provides no receiver-side expiry or replay prevention; a leaked current bundle remains usable by anyone who can reach the receiver or relay. Export again after rotation or an authority change. Deletion does not revoke the secrets; leaked keys/salts require the corresponding source/relay or cluster-salt rotation.

The release's activation interface takes a named private regular file, not stdin. Prepare /run/cloudtaser-management as an existing caller-owned 0700 directory on verified non-swappable tmpfs, and run the exporter as its owner (root for this root-owned management path). Symlink components and existing outputs are refused, and non-tmpfs output is rejected. No disk-backed copies, secret environment variables, shell substitution, hand-written bundle, substituted bare key or console output. The source authority export-operator-introduction command checks private custody and tmpfs through the pinned directory descriptor at reservation and publication. It cannot attest swap configuration; verify non-swappable tmpfs separately. Export and activation each require explicit private source credential input: use a separate fresh stdin pipe for each; a named token file may be reused. Remove the introduction after confirmed activation or immediately if stopping before activation. Deletion does not revoke its secrets. The exporter's --dry-run reads no files, contacts no source/cluster and writes no output.

On the EU source VM, prepare the verified runtime directory and export the handoff:

test "$(wc -l < /proc/swaps)" -eq 1
test "$(findmnt -n -o FSTYPE -T /run)" = tmpfs
install -d -o root -g root -m 0700 /run/cloudtaser-management
customer-root-token | cloudtaser-cli source authority export-operator-introduction \
  --instance eu --fingerprint "$CLUSTER_UID" --kubeconfig "$KUBECONFIG_FILE" \
  --secretstore-address "$SOURCE_ORIGIN" --secretstore-ca-file "$SOURCE_CA" \
  --secretstore-token-file - \
  --output /run/cloudtaser-management/operator-introduction.json

Save the following public configuration as external-port.yaml:

pdb:
  enabled: false
operator:
  ha: false
  replicaCount: 1
  secretBackend: inmemory
  enableReverseConnect: true
  broker:
    beacon:
      enabled: true
      address: 198.51.100.20:443
      trustSystemRoots: false
      # caCert is supplied from the verified public CA file via --set-file below.
      authKeyVolume:
        enabled: false
      runtimeAuthKey:
        enabled: true
        wireV2: true

Set the actual relay address and approved CA chain in BEACON_CA. Both Helm commands below embed this public PEM with --set-file and keep system-root trust disabled, so a private relay CA works without bypassing TLS. The core chart does not deploy Port; omit port.enabled, which its schema refuses. Site values must cover API, DNS, beacon, registry, webhook and health traffic at the actual CNI filtering stage. This profile selects one operator and disables its HA/PDB, while retaining production enforcement. It provides no operator failover. HA and unattended Port renewal need separate qualification; never bypass readiness.

Save this template to $SITE_VALUES, then replace every example address with reviewed site coordinates. Keep both chart and operator policy enabled. brokerIngressNamespaceSelector must select payments.

networkPolicy:
  apiServerCIDRs: ["10.96.0.1/32", "203.0.113.10/32"]
  apiServerPorts: [443]
  dnsCIDRs: ["169.254.20.10/32", "10.96.0.10/32"]
  webhookIngressCIDRs: ["192.0.2.30/32"]
  healthProbeCIDRs: ["192.0.2.40/32"]
  registryCIDRs: ["203.0.113.50/32"]
  beaconCIDR: "198.51.100.20/32"
  brokerIngressNamespaceSelector:
    matchLabels:
      kubernetes.io/metadata.name: payments
Input How to discover and review it
apiServerCIDRs, apiServerPorts kubectl get svc kubernetes -n default -o yaml, kubectl get endpointslice -n default -l kubernetes.io/service-name=kubernetes -o yaml, and the kubeconfig server. Include Service and post-DNAT backend addresses/ports observed at your CNI.
dnsCIDRs Inspect an unprotected Pod's /etc/resolv.conf, NodeLocal configuration and the kube-dns Service IP. Include both NodeLocal and kube-dns Service addresses where used; see the worked example below.
webhookIngressCIDRs Before install, inventory control-plane/konnectivity candidates and approve their bounded source ranges; see site network inputs. Validate actual sources after install. API destination addresses do not establish these sources.
healthProbeCIDRs Node/probe source addresses observed by the CNI; an empty list is appropriate only with verified node-probe exemption.
registryCIDRs Resolve accepted image/signature/auth hosts and layer redirects. A stale list can deny protected Pod creation after install when wrapper-init signature checks cannot reach the registry (inferred, not observed). The initial dated lab walk allowed all 132 Google IPv4 prefixes from goog.json, allowing operator TCP 443 egress to anything in those ranges. Accept that widening explicitly; a stable mirror is the narrower option. A single DNS snapshot is insufficient.
beaconCIDR Your fixed relay IP as /32 (/128 for IPv6); use beaconCIDRs for additional approved addresses.

Use site network inputs to gather the pre-install inventory and GKE example. The initial dated lab walk's single-node cluster used NodeLocal 169.254.20.10/32 and kube-dns Service 34.118.224.10/32. Its webhook candidate set was 10.124.0.4/32, 10.128.0.2/32, 10.128.0.4/32; installation and admission passed, but the test did not establish which source entry mattered, or observe a konnectivity path. These are test-site coordinates, not defaults. A konnectivity Pod IP can move on reschedule; stale /32 rules can block Pod CREATE/UPDATE, binding, exec, attach, port-forward, proxy and ephemeral-container operations in enrolled namespaces with failurePolicy: Fail. Address reuse can give another Pod access to the operator webhook on TCP 9443. Agent count can scale with nodes; cover every agent or expect possible intermittent failures (multi-node behavior was not observed). Approve bounded ranges and a refresh procedure before install, then validate at the CNI filtering stage after install. Unknown/unreviewed coordinates block install; a reasoned candidate set is not an observed traffic proof. Refreshing site values requires an untested helm upgrade with all step 6 flags: the pinned archive, --namespace cloudtaser-system, ordered -f "$PRODUCTION_VALUES" -f "$PLATFORM_VALUES" -f "$SITE_VALUES" -f external-port.yaml, --set-file operator.broker.beacon.caCert="$BEACON_CA" and --set-string operator.fingerprint.clusterID="$CLUSTER_UID", using the same explicit kubeconfig. Pre-upgrade hooks run. Check for an operator rollout and repeat the fresh private introduction below if a receiver loses RAM state. The US broker does not need direct store egress. Keep the private source address out of this relay profile's allowed destinations.

KUBERNETES_VERSION=$(kubectl --kubeconfig "$KUBECONFIG_FILE" version -o json | jq -r .serverVersion.gitVersion)
helm template cloudtaser "$CORE_CHART_ARCHIVE" \
  --namespace cloudtaser-system --kube-version "$KUBERNETES_VERSION" \
  -f "$PRODUCTION_VALUES" -f "$PLATFORM_VALUES" -f "$SITE_VALUES" \
  -f external-port.yaml \
  --set-file operator.broker.beacon.caCert="$BEACON_CA" \
  --set-string operator.fingerprint.clusterID="$CLUSTER_UID" \
  > cluster-public-render.yaml

Review cluster-public-render.yaml before installation: accepted image digests, init image, signatures, namespace selectors and all site traffic rules. Chart 1.0.359's GKE overlay header says not to stack it with production; the ordering above is the tested one, and the header needs correction in the next chart. Use a newly created dedicated namespace; stop if that name already exists. Record its UID for the step 9 ownership check. If a later command in this block fails, rerun only the failed command after resolving its cause; restarting the block stops at AlreadyExists under set -e:

kubectl --kubeconfig "$KUBECONFIG_FILE" create namespace cloudtaser-system
kubectl --kubeconfig "$KUBECONFIG_FILE" get namespace cloudtaser-system \
  -o jsonpath='{.metadata.uid}' > /root/cloudtaser-release/cloudtaser-system.uid
test -s /root/cloudtaser-release/cloudtaser-system.uid
helm upgrade --install cloudtaser "$CORE_CHART_ARCHIVE" \
  --namespace cloudtaser-system --create-namespace \
  -f "$PRODUCTION_VALUES" -f "$PLATFORM_VALUES" -f "$SITE_VALUES" \
  -f external-port.yaml \
  --set-file operator.broker.beacon.caCert="$BEACON_CA" \
  --set-string operator.fingerprint.clusterID="$CLUSTER_UID"
customer-root-token | cloudtaser-cli target activate-beacon-auth \
  --operator-introduction-file /run/cloudtaser-management/operator-introduction.json \
  --namespace cloudtaser-system --kubeconfig "$KUBECONFIG_FILE" \
  --secretstore-address "$SOURCE_ORIGIN" \
  --secretstore-ca-file /etc/cloudtaser/source-ca.pem --secretstore-token-file - \
  --wait --timeout 5m
kubectl --kubeconfig "$KUBECONFIG_FILE" -n cloudtaser-system \
  rollout status deployment/cloudtaser-operator --timeout=5m
rm /run/cloudtaser-management/operator-introduction.json

Retain the actual runtime image IDs after install. The render uses the live target Kubernetes version. The Helm commands run with KUBECONFIG="$KUBECONFIG_FILE" in their environment (set it explicitly in that session). Do not use Helm --wait before introduction: pending receivers cannot become Ready until they receive private material. Remove the handoff file after confirmed delivery; removal from tmpfs is not a heap-erasure guarantee.

The external Linux Port is already running, so this path does not install a second Port in the target cluster. target install --port-authority-instance belongs to the separate managed Kubernetes-Port composition (--install-port); it is not a flag to pair this external VM. No target connect --authority-instance command is part of this procedure.

For a replaced operator replica, obtain a fresh validated private handoff and repeat target activate-beacon-auth with the same explicit kubeconfig/namespace, verified source address/CA and a fresh private source-root input pipe. Activation compares the supplied introduction against source state before delivery and compares source pairing again after all desired owned replicas become Ready. Keep healthy replicas running; never rotate source salts, rerun provisioning or send a bare key to fix one pending receiver. Partial delivery is not success; inspect the pending receiver identity and reconcile through the release's reintroduction procedure. This manual profile needs reintroduction after RAM loss.

7. Deliver a secret to the application

Real-token beacon delivery passed both with explicit application resource budgets and with no resources block. Repeat these checks for your application. Confirm that the real projected token follows the beacon envelope path, the application child executes, and the source audit records the exact approved workload read.

Step 4 already created payments and reader. Download and review the complete protected-app.yaml, then apply it below. It includes the scoped CloudTaserPolicy, protected-reader Deployment and Service. Its Pod template explicitly selects one container (app) and includes injection, exact path, env map, restart rotation and cloudtaser.io/secretstore-auth-method: "token". Broker envelope mode requires that auth annotation and the policy; it needs no VAULT_TOKEN.

if test -n "${DOCS_ARTIFACT_DIR:-}"; then
  cp "$DOCS_ARTIFACT_DIR/assets/manifests/protected-app.yaml" protected-app.yaml
else
  curl --fail --silent --show-error --proto '=https' \
    https://docs.cloudtaser.io/assets/manifests/protected-app.yaml -o protected-app.yaml
fi

Application resource limits are optional. The no-resources workload passed with operator v0.13.209: missing limits stayed absent. The operator adds overhead to existing limits. LimitRange-defaulted limits are also adjusted by source behavior; that case was not tested. Explicit CPU and memory limits of 400m and 192Mi became 500m and 256Mi; requests of 100m and 128Mi became 150m and 160Mi. Size budgets for your own app; the wrapper and child share the container's cgroup budget.

Size the effective limit, including tmpfs

Each protected Pod carries about 42 MB of wrapper/shim/library files in memory-backed cloudtaser-bin, charged to the Pod cgroup but absent from kubectl top. Idle Python peaked at 26 MB; the Pod cgroup used 67–69 MB. The operator adds 64Mi to a memory limit you set, covering that 42 MB; do not add it twice. The two OOM runs used a manifest limit of 1Mi, becoming an effective 65Mi after mutation, with no CPU limit. Keep the effective budget well above 69 MB for this fixture; measure your own app's peak Pod cgroup usage and add headroom. A manifest limit of 192Mi became 256Mi and passed. Missing limits remain absent.

Historical capped Pods retain their 64Mi limit until recreated; container restarts cannot change it. For existing installations and downgrade risks, read upgrade and rollback limits.

The live fixture was Python with libc getenv and os.environ, not a C getter. The published synthetic fixture below reads libc getenv and exposes only its SHA256 through /status and /compare. The initial dated lab walk ran this fixture on chart 1.0.359 with released CLI v0.19.173: v1/v2 Service checks and source audit matched. The pinned image, executable, environment API and delivery mode still need qualification together for your app; see runtime compatibility.

The checks use an unwrapped in-cluster client in payments-checks and the Service, without an external route or protected-container exec. HTTP here is only the synthetic digest fixture; never use it to expose real secret values. The helper retries while a single-replica rollout settles:

check-marker() {
  kubectl --kubeconfig "$KUBECONFIG_FILE" -n payments-checks run marker-check \
    --restart=Never --rm --attach --pod-running-timeout=5m --image=docker.io/library/python@sha256:2d44738ebf4ce5f63b9d9ea871e9c7cfdc0426f7b0c97cac915fd88ded1d86bb \
    --command -- python3 -c '
import json, sys, time, urllib.request
url = "http://protected-reader.payments.svc.cluster.local:8080/compare?sha256=" + sys.argv[1]
for attempt in range(60):
    try:
        with urllib.request.urlopen(url, timeout=5) as response:
            result = json.load(response)
        if result.get("matches") is True:
            print("marker digest matches through Service")
            break
    except (OSError, ValueError):
        pass
    time.sleep(2)
else:
    raise SystemExit("Service marker check failed")
' "$1"
}
kubectl --kubeconfig "$KUBECONFIG_FILE" apply -f protected-app.yaml
kubectl --kubeconfig "$KUBECONFIG_FILE" -n payments \
  rollout status deployment/protected-reader --timeout=5m
cloudtaser-cli target status --kubeconfig "$KUBECONFIG_FILE" --namespace payments
check-marker "$MARKER_V1_SHA256"
kubectl --kubeconfig "$KUBECONFIG_FILE" -n payments get pods
kubectl --kubeconfig "$KUBECONFIG_FILE" -n payments get endpointslices

Verify through the application's own endpoint that it consumed marker v1; retain only its digest, Ready/EndpointSlice membership and source audit scope. Wrong-SA/path authorization also passed in the initial dated lab walk on chart 1.0.359. Repeat those negative cases for your app before qualifying it. Do not use exec into the protected process or equate an empty exec result with success. A Port launch receipt or Ready operator alone does not qualify the application.

Restrict the source listener on the host

The deadline and firewall lab walk ran the IPv4 host chain and CA-verified firewall check as written, after step 7 with Port running. Under the chain, three unprotected-Pod probes timed out, the DROP counter rose by 15 packets, and nine rate-limited kernel log lines all named that Pod's source IP and destination port 8200. The relay received HTTP 200; step 8's v2 rotation became Ready, matched the digest and recorded an audited version-2 read through the source host. A fresh Port launch under the chain also passed, with a 9.06-second relaunch gap. Enrollment and a reboot under the chain remain untested. The installer binds the private address but installs no host firewall. Before these rules, an unprotected Pod reached TLS /v1/sys/health on :8200 (HTTP 200). The chart's protected-Pod policy does not restrict unprotected Pods. Keep only the synthetic value in the store until the firewall check passes.

On your dedicated source host, review existing firewall management and replace the example coordinates first. This chain permits loopback, the source itself and the approved relay, then drops other inbound TCP/8200. Port's store traffic arrives on lo, even when it uses the source's private IP; retain that rule. Add exact approved management peers before the drop if your deployment needs remote store administration. The example assumes the IPv4 addresses used here; it does not cover an IPv6 listener or configure persistent firewall storage. A reboot or firewall reload removes these rules or their INPUT jump, leaving :8200 open until protection is reapplied. Inspect and restore the reviewed rules and jump, then rerun the store firewall check after any reboot or firewall reload, before resuming delivery.

Before applying the chain, record the same unprotected client's baseline. The deadline and firewall lab walk's --rm --attach baseline returned zero but lost its success line when the completed Pod was deleted. This block retains the Pod until its successful completion and HTTP-200 log line have both been checked, then deletes it. This revised baseline has not been run live. A failed run leaves the Pod behind; delete that store-firewall-baseline Pod in payments-checks before retrying, or kubectl run stops at AlreadyExists. Run in session 2 on the source/management host after step 7; require HTTP 200:

set -euo pipefail
kubectl --kubeconfig "$KUBECONFIG_FILE" -n payments-checks run store-firewall-baseline \
  --restart=Never \
  --image=docker.io/library/python@sha256:2d44738ebf4ce5f63b9d9ea871e9c7cfdc0426f7b0c97cac915fd88ded1d86bb \
  --command -- python3 -c '
import ssl, sys, urllib.request
context = ssl.create_default_context(cadata=sys.argv[2])
with urllib.request.urlopen(sys.argv[1] + "/v1/sys/health", context=context, timeout=5) as response:
    if response.status != 200:
        raise SystemExit("FAIL: expected healthy store baseline")
    print("baseline: store HTTP 200", flush=True)
' "$SOURCE_ORIGIN" "$(cat "$SOURCE_CA")"
kubectl --kubeconfig "$KUBECONFIG_FILE" -n payments-checks wait \
  --for=jsonpath='{.status.phase}'=Succeeded --timeout=5m pod/store-firewall-baseline
kubectl --kubeconfig "$KUBECONFIG_FILE" -n payments-checks logs store-firewall-baseline | \
  grep -Fx 'baseline: store HTTP 200'
kubectl --kubeconfig "$KUBECONFIG_FILE" -n payments-checks delete pod store-firewall-baseline

If protection is already in place, retain the reviewed baseline; do not remove a working firewall just to obtain one. An upstream drop or a stalled listener is not an acceptable baseline. Then apply the reviewed host rules:

set -euo pipefail
SOURCE_IP="${SOURCE_LISTEN_ADDRESS%:*}"
iptables -N CT-STORE
iptables -A CT-STORE -i lo -j ACCEPT
iptables -A CT-STORE -s "$SOURCE_IP/32" -j ACCEPT
iptables -A CT-STORE -s "$RELAY_SOURCE_IP/32" -j ACCEPT
iptables -A CT-STORE -m limit --limit 20/min -j LOG --log-prefix 'CT-STORE-DROP '
iptables -A CT-STORE -j DROP
iptables -I INPUT 1 -p tcp --dport 8200 -j CT-STORE
iptables -S CT-STORE
iptables -C INPUT -p tcp --dport 8200 -j CT-STORE

Do not rerun chain creation after a partial success; inspect the exact rules. This does not change a shared cloud firewall. In the 1 November 2026 verification walk the lab cloud rule still allowed Pods to :8200. With the host chain, three unprotected-Pod probes timed out, the relay got HTTP 200, and a new protected Pod received v3 with an audited store read through loopback. After chain removal, the Pod got 200 again. Run the following store firewall check now, before step 8.

Store firewall check

Test the host chain applied after step 7 from an unprotected Pod; a protected-Pod timeout alone could be its NetworkPolicy. This uses the public CA and no store token. Require the retained HTTP-200 baseline, the INPUT jump, three timeouts and at least three additional packets on rule 5's DROP counter during this probe. Timeouts alone also occur with upstream drops or a listener that accepts and stalls. An HTTP response, TLS failure or other error fails the check. Review the exact chain first: rule 5 must be its final DROP, with approved allow rules ahead of it. If management rules changed its number, update the counter selector to that reviewed DROP rule. Run without concurrent store probes, and correlate any other traffic with host logs; unrelated drops can also raise the counter. Rerun this check after every reboot or firewall reload.

set -euo pipefail
iptables -S CT-STORE
iptables -C INPUT -p tcp --dport 8200 -j CT-STORE
store-drop-count() {
  iptables -L CT-STORE -v -n -x --line-numbers | \
    awk '$1 == 5 && $4 == "DROP" { count = $2; found++ }
         END { if (found != 1) exit 1; print count }'
}
STORE_DROPS_BEFORE=$(store-drop-count)
kubectl --kubeconfig "$KUBECONFIG_FILE" -n payments-checks run store-firewall-check \
  --restart=Never --rm --attach --pod-running-timeout=5m \
  --image=docker.io/library/python@sha256:2d44738ebf4ce5f63b9d9ea871e9c7cfdc0426f7b0c97cac915fd88ded1d86bb \
  --command -- python3 -c '
import socket, ssl, sys, urllib.error, urllib.request
context = ssl.create_default_context(cadata=sys.argv[2])
for attempt in range(3):
    try:
        urllib.request.urlopen(sys.argv[1] + "/v1/sys/health", context=context, timeout=5)
    except (TimeoutError, urllib.error.URLError) as exc:
        if not isinstance(getattr(exc, "reason", exc), (TimeoutError, socket.timeout)):
            raise
        print("store probe timed out", flush=True)
    else:
        raise SystemExit("FAIL: unprotected Pod reached store")
' "$SOURCE_ORIGIN" "$(cat "$SOURCE_CA")"
STORE_DROPS_AFTER=$(store-drop-count)
test "$((STORE_DROPS_AFTER - STORE_DROPS_BEFORE))" -ge 3
iptables -C INPUT -p tcp --dport 8200 -j CT-STORE
iptables -L CT-STORE -v -n -x --line-numbers

On the relay host, with its verified public exports, require HTTP 200:

curl --fail --silent --show-error --cacert "$SOURCE_CA" \
  --max-time 10 --output /dev/null --write-out '%{http_code}\n' \
  "$SOURCE_ORIGIN/v1/sys/health"

After both checks pass, continue in session 2 with step 8's v2 rotation, require Ready and verify MARKER_V2_SHA256 through check-marker. Confirm the exact approved version-2 read in the source audit and that Port remains active. This distinguishes allowed loopback delivery from the blocked direct Pod-to-store path. Keep the reviewed host chain in place; the measured lab runs do not establish a default product firewall.

8. Rotate and test outage recovery

Write v2, then explicitly roll the selected protected Deployment:

BAO_ADDR="$SOURCE_ORIGIN" BAO_CACERT="$SOURCE_CA" \
  BAO_TOKEN="$(customer-root-token)" \
  bao kv put secret/payments/credentials marker=preview-v2
cloudtaser-cli target rotate --kubeconfig "$KUBECONFIG_FILE" \
  --namespace payments --workload deployment/protected-reader --dry-run
cloudtaser-cli target rotate --kubeconfig "$KUBECONFIG_FILE" \
  --namespace payments --workload deployment/protected-reader
kubectl --kubeconfig "$KUBECONFIG_FILE" -n payments \
  rollout status deployment/protected-reader --timeout=5m
check-marker "$MARKER_V2_SHA256"

Verify the application's digest changes to v2 through the Service. This is explicit restart-based rotation; do not claim automatic static-KV polling through the beacon or snapshot updates on SIGHUP without a native receipt.

The 1 November 2026 verification walk on chart 1.0.359, operator v0.13.209, Port v0.4.133, observed:

Dependency failure Running application Operator New Pod and recovery
Relay stopped Served throughout, no failed probes Session ended after drain; Ready false 27 seconds after deactivation, Ready again 22 seconds after relay start Stop took 30 seconds to drain, during which a new Pod still received its value. Truly down for 18 seconds. Measure outage from deactivation, not the stop request.
Source sealed Kept serving Stayed Ready; Port stayed active Stayed sealed, warning failed to decrypt KEM unseal payload; Ready about 23 seconds after unseal, zero container restarts. The initial dated lab walk had one restart; a restart is not required in every run.
Terminal Ctrl-C of Port Kept serving Ready false after stop Fresh launch recovered a sealed Pod in about 10 seconds without recreation or operator reintroduction; source audit confirmed v4.
One-hour approval deadline Zero failed probes through the 19-minute gap Lost Ready, retried automatically without a container restart New Pods stayed sealed until fresh launch; see step 5 for deadline, token and recovery evidence.

The 90-second unchanged-value check also ran on this release: after writing v3, 97 probes over 96.7 seconds still returned v2, with the same Pod UID, zero restarts and no audited secret reads. Explicit target rotate then delivered v3. This does not establish automatic KV-v2 polling or SIGHUP refresh.

Protected-Pod egress boundary

The 1 November 2026 verification walk used a purpose-built protected Deployment with the same annotations and reader SA, confirmed marker delivery, and compared it with an unprotected control. In 55 retained rounds, the protected Pod could not reach 1.1.1.1:443, 8.8.8.8 TCP/UDP 53, the registry :443, store :8200, relay :443/:8200, metadata :80/:988, kubelet :10250, a Pod sink :9000 or ICMP. The control reached those targets over 140 rounds; both sinks saw only the control's IP.

The following channels stayed open from the protected Pod:

  • kube-dns and NodeLocal DNS on TCP/UDP 53; example.com resolved every round.
  • The API server on service, private and public addresses, all TCP 443.
  • The node on TCP 19099.
  • The operator on TCP 8199 (some rounds saw connection refused).

This measures CNI connection blocking for a labelled Pod on this single-node IPv4 lab. It does not prove whether DNS tunnelling is possible (no exfiltration was attempted), API access with a token, IPv6 behavior, label-tampering resistance, a real application's behavior or multi-node isolation.

The 1 November 2026 verification walk's app continued serving after its store tokens were removed at the deadline; this does not prove refresh or fresh reads during an outage. Operator Ready alone did not detect a sealed source. No outage SLA or general application continuity is established by these observations.

Known startup behaviors from the runs:

  • Signature verification denied Pod CREATE for up to 90 seconds before succeeding; retain events and retry after the cache warms, keeping verification enabled.
  • A false critical SECRET LEAK DETECTED alert and initial sidecar DNS loss were observed; investigate against source/audit evidence rather than treating the alert alone as proof of a leaked value.
  • A Pod admitted before pairing stays sealed until recreated after pairing.

Repeat controlled tests on dedicated synthetic resources for your application. Terminate only the owned Port management parent, stop only the owned relay service and seal only the owned test source, separately. Record the serving value digest, restarts, Ready/EndpointSlice membership, new-Pod behavior and operator state; do not edit shared firewalls. Restore the dependency, use the retained unseal custody if needed, and freshly admit Port if its finite session ended. Reintroduce any operator receiver that lost its RAM state. Confirm the current value and Service traffic after recovery. Keep application-owned outage policy explicit and enforcement enabled throughout.

Pod deletion is not immediate token revocation. Issued store tokens can remain valid until TTL, up to 15 minutes; the tested product revocation attempts were denied. Do not use Pod deletion as credential invalidation or assume that this guide's host-grant cleanup revokes independently issued tokens. Use reviewed source-administrator incident handling when immediate invalidation is required.

9. Uninstall and retire authority deliberately

Drain/delete only the test application before removing enforcement:

kubectl --kubeconfig "$KUBECONFIG_FILE" -n payments delete deployment protected-reader
kubectl --kubeconfig "$KUBECONFIG_FILE" -n payments wait --for=delete pod \
  -l app=protected-reader --timeout=5m
KUBECONFIG="$KUBECONFIG_FILE" helm uninstall cloudtaser -n cloudtaser-system --timeout 5m

Uninstall removes CRDs and their CloudTaserPolicy objects; reapply the policy manifest after reinstall. A hook refusal for a still-Terminating protected Pod removes nothing: wait for it to disappear, then rerun the same uninstall safely. Do not infer complete cleanup from Helm's exit status alone. Inspect remaining release-owned hooks and webhook objects:

kubectl --kubeconfig "$KUBECONFIG_FILE" -n cloudtaser-system get jobs
kubectl --kubeconfig "$KUBECONFIG_FILE" get mutatingwebhookconfigurations,validatingwebhookconfigurations

An unresolved retire-hook failure or an orphan/recreated release webhook is a teardown failure, not a successful uninstall. Do not force-delete hooks or broadly delete shared webhooks. Identify exact release ownership and use its approved cleanup procedure; keep failure evidence. The matched chart's pre-delete enforcement cleanup must complete; retain its retire-hook receipt. This guide supplies no independent node-attachment inspection command, so Helm success alone is not that proof. Do not remove a protection agent while protected workloads remain. Stop the finite host session before its deadline, as in step 5. Terminal Ctrl-C was observed live in the 1 November 2026 verification walk: port_stopped, rc=1 and context canceled, with the shell left open. Approval-deadline stop has the different message documented in step 5. This procedure installed no persistent Port systemd service. Skip the following deletion block on any teardown failure. Require successful uninstall, completed cleanup hooks, no protected workloads or remaining agents, an empty Helm release list and no unexpected resources in the inventory below. Compare the live namespace UID against the UID recorded in step 6; if the record is absent or differs, stop. Confirm the other dedicated names are still yours. Only then remove the test Service and preflight RBAC/namespaces:

set -euo pipefail
test -s /root/cloudtaser-release/cloudtaser-system.uid
test "$(kubectl --kubeconfig "$KUBECONFIG_FILE" get namespace cloudtaser-system -o jsonpath='{.metadata.uid}')" \
  = "$(cat /root/cloudtaser-release/cloudtaser-system.uid)"
REMAINING_RELEASES=$(KUBECONFIG="$KUBECONFIG_FILE" helm list -n cloudtaser-system \
  --deployed --failed --pending --superseded --uninstalling --uninstalled -q)
test -z "$REMAINING_RELEASES"
kubectl --kubeconfig "$KUBECONFIG_FILE" get pods -A -l cloudtaser.io/protected=true
kubectl --kubeconfig "$KUBECONFIG_FILE" -n cloudtaser-system get all
kubectl --kubeconfig "$KUBECONFIG_FILE" -n payments get all
kubectl --kubeconfig "$KUBECONFIG_FILE" -n payments-checks get all
read -r -p 'Cleanup hooks complete, no protected workloads/agents, and payments, payments-checks, cloudtaser-system safe to delete? Type yes: ' CLEANUP_REVIEW </dev/tty
test "$CLEANUP_REVIEW" = yes
kubectl --kubeconfig "$KUBECONFIG_FILE" -n payments delete service protected-reader
kubectl --kubeconfig "$KUBECONFIG_FILE" delete clusterrolebinding payments-token-reviewer
kubectl --kubeconfig "$KUBECONFIG_FILE" delete clusterrole payments-token-reviewer
kubectl --kubeconfig "$KUBECONFIG_FILE" delete namespace payments payments-checks cloudtaser-system

After each host approval expires, set HOST_APPROVAL_FILE to that launch's own retained original absolute path, then run step 5's exact source host-launch reconcile command and require rc=0, remaining_launch_tokens=0 and remaining_grants=0 for each launch. Do not let restoring step 1's exports select its example path or another launch's approval. Reconcile removes the host launch's AppRole/policy and counts remaining login tokens; it does not revoke them. If tokens remain, follow step 5's revoke --confirm-offline procedure and reconcile again. The offline revoke can also retire a stopped launch before expiry. Keep the receipt's unverified-descendant limits and cluster token scan requirement; Helm uninstall does not establish store-token retirement. There is no complete retained-store grant-retirement procedure in this guide: the initial dated lab walk still had one AppRole role and four source policies. For this fresh dedicated test source, retiring the dedicated source VM and its store disk is the complete source-retirement path, after the source owner's retention/backup decision and token-TTL/incident handling. Do not delete them while custody or retained data is still required. If retaining the store, source grant retirement remains unresolved and needs reviewed administrator maintenance; cluster uninstall and host reconciliation do not complete it. Your relay administrator disables only this tenant after checking for other consumers. No blanket managed authority-delete command or automatic cross-system cleanup is assumed. Keep application data, OpenBao volumes, audit records and unseal custody until the retention/backup decision. Never delete a shared relay/store or purge data as a side effect of uninstalling the cluster chart.

For a dedicated relay, after both source/cluster retirement and confirmation that no other consumer needs it, stop cloudtaser-beacon-strict.service. Retire only this tenant's private runtime map/credential custody under your retention policy; never clear a shared tenant map. Keep OpenBao data and audit/unseal custody unless the source owner separately authorizes destruction. This is scoped EU teardown, not a blanket store delete. A failed host launch's exact ct-port-host-* grants also need reconciliation even if no Port remains running.

For dedicated test VMs/disks and namespaces, record kind/name/UID ownership before creation and compare the final inventory with that baseline. Retire only proven-owned resources after grant and custody reconciliation. Shared networks, firewalls and unrelated workloads remain outside that teardown. The initial dated lab walk passed the owned-resource inventory comparison and expired host-grant reconciliation on the retained source. It did not establish complete source-grant retirement there. Repeat the inventory comparison for your installation and include cloudtaser-system, which Helm uninstall itself leaves behind.