Before you deploy
TrustedAIProxy runs at the gateway's egress. Only internal callers should reach its proxy port. End users do not connect to the proxy or install its internal MITM CA; they establish trust through response headers and a customer-accessible proof endpoint.
What you need
- A Google Cloud project with billing enabled and an authenticated
gcloudCLI; - Permissions to create Compute Engine, VPC, firewall, service account, and Secret Manager resources;
- Access to pull the immutable TrustedAIProxy image specified here from GHCR;
- Internal network connectivity between TAP and a gateway such as New API / One API;
- Customer-approved project, instance, image digest, upstream, and attestation policies.
Official Google references: Deploy Confidential Space workloads, Image types and lifecycle, Supported hardware and regions.
Set deployment variables
This example uses AMD SEV with n2d-standard-2. First confirm that your zone supports the CPU. If you switch to TDX, update the machine type, zone, and customer-approved hwmodel together.
export PROJECT_ID="your-project"
export REGION="us-central1"
export ZONE="us-central1-a"
export INSTANCE_NAME="tap-01"
export MACHINE_TYPE="n2d-standard-2"
export SERVICE_ACCOUNT_NAME="tap-workload"
export SERVICE_ACCOUNT="${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"
export NETWORK="tap-vpc"
export SUBNET="tap-subnet"
export SUBNET_CIDR="10.40.0.0/24"
export INTERNAL_CALLER_CIDR="10.40.1.0/24"
export IMAGE="ghcr.io/tokenevol/trustedaiproxy@sha256:8271352838ac4db3647b4b028f6f6ddd67266d4a0114ada49db3980e5ed331ea"
gcloud config set project "${PROJECT_ID}"
gcloud compute zones describe "${ZONE}" \
--format="value(availableCpuPlatforms)"Replace PROJECT_ID, the zone, and the caller CIDR;IMAGE is already pinned to a pullable version. Deliver these values as part of the customer's out-of-band policy after deployment.
Enable the required APIs
gcloud services enable \
compute.googleapis.com \
confidentialcomputing.googleapis.com \
iam.googleapis.com \
logging.googleapis.com \
secretmanager.googleapis.comThis guide uses a public GHCR image directly. You do not need Artifact Registry or additional registry IAM permissions to pull it.
Create a workload identity
The VM's service account must be able to request Confidential Space attestation tokens. Logging permissions are for test diagnostics; production images should not redirect application logs by default.
gcloud iam service-accounts create "${SERVICE_ACCOUNT_NAME}" \
--display-name="TrustedAIProxy Confidential Space workload"
gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
--member="serviceAccount:${SERVICE_ACCOUNT}" \
--role="roles/confidentialcomputing.workloadUser"
gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
--member="serviceAccount:${SERVICE_ACCOUNT}" \
--role="roles/logging.logWriter"
gcloud iam service-accounts add-iam-policy-binding "${SERVICE_ACCOUNT}" \
--member="user:$(gcloud config get-value account)" \
--role="roles/iam.serviceAccountUser"Apply least privilege in production: remove logging.logWriter if you do not write to Cloud Logging. Grant Secret Manager read access to individual secrets, rather than the entire project.
Use a published immutable image
You do not need to build or publish an image yourself. Use the fixed digest already published to GHCR by this repository:
export IMAGE="ghcr.io/tokenevol/trustedaiproxy@sha256:8271352838ac4db3647b4b028f6f6ddd67266d4a0114ada49db3980e5ed331ea"Pinning a digest prevents silent code changes when a tag moves. The image digest in the customer's verification policy must match this value exactly.
Set up private networking and restricted ingress
This example creates a custom VPC, private subnet, and Cloud NAT. The VM has no public IP but can pull public images and reach model providers. Only the gateway's CIDR may access TAP on 8080/tcp.
gcloud compute networks create "${NETWORK}" --subnet-mode=custom
gcloud compute networks subnets create "${SUBNET}" \
--network="${NETWORK}" \
--region="${REGION}" \
--range="${SUBNET_CIDR}" \
--enable-private-ip-google-access
gcloud compute routers create tap-router \
--network="${NETWORK}" \
--region="${REGION}"
gcloud compute routers nats create tap-nat \
--router=tap-router \
--region="${REGION}" \
--auto-allocate-nat-external-ips \
--nat-all-subnet-ip-ranges
gcloud compute firewall-rules create tap-from-internal \
--network="${NETWORK}" \
--direction=INGRESS \
--action=ALLOW \
--rules=tcp:8080 \
--source-ranges="${INTERNAL_CALLER_CIDR}" \
--target-tags=tapIf the gateway is in another VPC, configure VPC Peering, a VPN, or another private connection first. Restrict INTERNAL_CALLER_CIDR to the actual callers. Do not create a 0.0.0.0/0 → tcp:8080 rule.
Prepare secrets with fixed versions
Single-replica tests can run without PostgreSQL. For production with multiple replicas, store the full PostgreSQL DSN in Secret Manager and pass only a resource name with a fixed numeric version. The service creates or migrates proof-related tables automatically.
gcloud secrets create tap-pg-dsn --replication-policy=automatic
printf '%s' 'postgres://tap:PASSWORD@postgres.internal:5432/tap?sslmode=verify-full' \
| gcloud secrets versions add tap-pg-dsn --data-file=-
gcloud secrets add-iam-policy-binding tap-pg-dsn \
--member="serviceAccount:${SERVICE_ACCOUNT}" \
--role="roles/secretmanager.secretAccessor"
export PG_SECRET_VERSION="projects/${PROJECT_ID}/secrets/tap-pg-dsn/versions/1"- Do not use
versions/latest; - Keep DSNs, passwords, and CA private keys out of Git, images, command-line arguments, and instance metadata;
- Enable database TLS verification and restrict network sources;
- The service currently reads only the PostgreSQL DSN directly from Secret Manager. A separate, controlled bootstrap must still be implemented for the production MITM CA.
Create a debug test instance
Use only synthetic data and the demonstration CA. Debug images allow SSH/root access;dbgstat is enabled, which production customer policies must reject.
gcloud compute instances create "${INSTANCE_NAME}-debug" \
--project="${PROJECT_ID}" \
--zone="${ZONE}" \
--machine-type="${MACHINE_TYPE}" \
--network="${NETWORK}" \
--subnet="${SUBNET}" \
--no-address \
--confidential-compute-type=SEV \
--maintenance-policy=MIGRATE \
--shielded-secure-boot \
--image-project=confidential-space-images \
--image-family=confidential-space-debug \
--service-account="${SERVICE_ACCOUNT}" \
--scopes=cloud-platform \
--tags=tap \
--metadata="^~^tee-image-reference=${IMAGE}~tee-restart-policy=OnFailure~tee-container-log-redirect=true"Inspect startup logs and the internal IP:
gcloud compute instances get-serial-port-output "${INSTANCE_NAME}-debug" \
--zone="${ZONE}" --port=1
export TAP_INTERNAL_IP="$(gcloud compute instances describe "${INSTANCE_NAME}-debug" \
--zone="${ZONE}" --format='get(networkInterfaces[0].networkIP)')"
echo "TAP internal address: http://${TAP_INTERNAL_IP}:8080"Check the metadata endpoints from an allowed internal host:
curl --fail --silent --show-error \
"http://${TAP_INTERNAL_IP}:8080/.well-known/http-attestation-key"
NONCE="$(openssl rand -hex 16)"
curl --fail --silent --show-error \
"http://${TAP_INTERNAL_IP}:8080/.well-known/confidential-attestation?nonce=${NONCE}"Create a production instance
Production requires a stable, securely provisioned MITM CA. Ed25519 signing keys are generated in memory for each process and bound to startup attestation. Ensure the container never logs message bodies, queries, credentials, tokens, or DSNs. If CA provisioning changes the image, update its pinned digest and the customer policy together.
gcloud compute instances create "${INSTANCE_NAME}" \
--project="${PROJECT_ID}" \
--zone="${ZONE}" \
--machine-type="${MACHINE_TYPE}" \
--network="${NETWORK}" \
--subnet="${SUBNET}" \
--no-address \
--confidential-compute-type=SEV \
--maintenance-policy=MIGRATE \
--shielded-secure-boot \
--image-project=confidential-space-images \
--image-family=confidential-space \
--service-account="${SERVICE_ACCOUNT}" \
--scopes=cloud-platform \
--tags=tap \
--metadata="^~^tee-image-reference=${IMAGE}~tee-restart-policy=OnFailure~tee-env-TAP_PG_DSN_SECRET_VERSION=${PG_SECRET_VERSION}"Remove tee-env-TAP_PG_DSN_SECRET_VERSION=… from metadata. The customer verifier must use --without-postgres and account for the lack of cross-replica routing for new challenges.
Production Confidential Space images disable SSH. Use minimal startup logs, internal health checks, and customer proof verification for operations. Do not switch a production instance back to a debug image for troubleshooting.
Configure your gateway to use TAP
Configure the proxy in the New API / One API gateway process and add the production Intermediate CA certificate to its system or application trust store. Only the internal gateway should trust this CA; end users must never be asked to install it.
export HTTP_PROXY="http://${TAP_INTERNAL_IP}:8080"
export HTTPS_PROXY="http://${TAP_INTERNAL_IP}:8080"
export NO_PROXY="127.0.0.1,localhost,metadata.google.internal"
# Import the production Intermediate CA as required by the gateway runtime.
# Do not disable TLS verification or use InsecureSkipVerify.Confirm that the gateway returns all X-Attestation-* headers unchanged. The proxy clears forged or stale upstream headers with these names and adds new values only after successfully generating a local attestation.
Production acceptance checklist
| Check | Acceptance criteria |
|---|---|
| Image | Deploy with @sha256:… and a digest that exactly matches the customer-approved value. |
| Runtime | The Google token must contain swname=CONFIDENTIAL_SPACE, dbgstat=disabled-since-boot, secboot=true and satisfy hardware and STABLE/LATEST policies. |
| Key material | No production CA private keys, signing private keys, DSNs, or credentials in images, Git, metadata, or logs. |
| Network | No public ingress; port 8080 accepts only the gateway's private CIDR; outbound TLS verification remains enabled. |
| Non-streaming signatures | Supported plain-text requests return llm-conversation-text-v1 and verify with the reference client. |
| Fail closed | Oversized, ambiguous, multimodal, or unsupported content is forwarded without locally generated attestation headers. |
| Streaming boundary | SSE with a valid challenge returns only llm-request-upstream-v1; the product UI explicitly marks the body as unverified. |
| Multiple replicas | proof_ref routes to its owner; offline owners, unknown references, and timeouts return the expected statuses. |
Finally, deliver the issuer, audience, image digest, project ID/number, zone, instance allowlist, service account, hardware type, allowed domains/paths/models, and fixed secret version through a trusted out-of-band channel. Customers must not learn these expected values from the response they are trying to verify.
Troubleshooting
| Symptom | Check first |
|---|---|
| Container does not start | Is the GHCR package public? Does the digest exist? Are Cloud NAT and DNS working? Is the image built for linux/amd64? |
| Proof endpoint returns 503 | Does the service account have roles/confidentialcomputing.workloadUser? Is the launcher socket accessible? Are the audience and nonce valid? |
| Missing attestation headers | Check the request path, Content-Type, body size, text extractor, upstream TLS, and stream/challenge requirements for the profile. |
| Gateway request fails TLS | Confirm that the gateway process trusts the correct internal CA, rather than an old CA. Do not disable certificate verification. |
| proof_ref lookup returns 404/410/503 | 404 = unknown reference; 410 = owner offline; 503 = cross-replica issuance timeout or temporary failure. Check PostgreSQL and owner heartbeats. |
| Customer verification fails | Has the gateway changed the model, path, roles, order, message boundaries, or text? Were headers dropped or merged? Has the nonce already been consumed? |