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:
-
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 -
Stop the current Port with Ctrl-C well before its deadline in its attached launch session; wait for
port_stoppedand inspect the exit as described above. -
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.
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.comresolved 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 DETECTEDalert 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.