GCE userspace VPC tunnels
Run an alpha VPC tunnel agent on a Google Compute Engine VM and publish a private service through the default Gateway.
This guide runs an Apoxy VPC tunnel agent on a Google Compute Engine VM without a kernel TUN device. The VM reads its Apoxy credential from Secret Manager, starts a dual-stack demo service, and connects to the relay in userspace mode.
The request path is:
HTTPS → Gateway → HTTPRoute → Backend → VPCService → relay → GCE VM → serviceCheck prerequisites
- The Apoxy CLI is authenticated to the intended project.
- The managed Apoxy project is ready.
- The Google Cloud CLI is authenticated to a project where you can create Compute Engine, IAM, and Secret Manager resources.
jqis installed locally.
Choose the relay nearest the VM from apoxy vpc relay list, then set the demo values:
apoxy auth --check
apoxy gateway get default
apoxy vpc network get default
apoxy vpc relay list
export GCP_PROJECT=<your-project-id>
export GCP_ZONE=<your GCP zone>
export GCP_NETWORK=<your GCP network>
export GCP_SUBNET=<your GCP subnet>
export INSTANCE_NAME=apoxy-tunnel-agent
export SERVICE_ACCOUNT_NAME=apoxy-tunnel-agent
export SECRET_NAME=apoxy-vpc-token
export AGENT_NAME=gce-private-api
export RELAY_ADDRESS=<closest relay address>.relay.apoxy.net:6081
gcloud auth list --filter=status:ACTIVE
gcloud config set project "$GCP_PROJECT"
gcloud services enable \
compute.googleapis.com \
iam.googleapis.com \
secretmanager.googleapis.comStore the tunnel credential
Create a dedicated service account and store the default VPC network's token without a trailing newline:
gcloud iam service-accounts create "$SERVICE_ACCOUNT_NAME" \
--display-name="Apoxy VPC tunnel agent"
export SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${GCP_PROJECT}.iam.gserviceaccount.com"
APOXY_VPC_TOKEN="$(
apoxy vpc network get default -o json | jq -jr '.status.credentials.token'
)"
test -n "$APOXY_VPC_TOKEN"
printf '%s' "$APOXY_VPC_TOKEN" | gcloud secrets create "$SECRET_NAME" \
--replication-policy=automatic \
--data-file=-
unset APOXY_VPC_TOKEN
gcloud secrets add-iam-policy-binding "$SECRET_NAME" \
--member="serviceAccount:${SERVICE_ACCOUNT_EMAIL}" \
--role=roles/secretmanager.secretAccessorOnly the dedicated service account can read this secret. The VM receives the secret's name and other non-secret settings as instance metadata; the credential itself is never stored there.
Create the VM
Download the maintained startup script:
curl --fail --silent --show-error \
--output apoxy-tunnel-agent-gce.sh \
https://apoxy.dev/docs/templates/apoxy-tunnel-agent-gce.shCreate a Debian 12 VM with the dedicated identity:
gcloud compute instances create "$INSTANCE_NAME" \
--zone="$GCP_ZONE" \
--machine-type=e2-small \
--image-family=debian-12 \
--image-project=debian-cloud \
--boot-disk-type=pd-balanced \
--boot-disk-size=20GB \
--network="$GCP_NETWORK" \
--subnet="$GCP_SUBNET" \
--service-account="$SERVICE_ACCOUNT_EMAIL" \
--scopes=cloud-platform \
--metadata="apoxy-vpc-secret=${SECRET_NAME},apoxy-agent-name=${AGENT_NAME},apoxy-vpc-name=default,apoxy-relay-address=${RELAY_ADDRESS}" \
--metadata-from-file=startup-script=apoxy-tunnel-agent-gce.shThe script installs Docker, copies the Secret Manager value to a root-only file, and runs a pinned
multi-architecture Apoxy image with host networking. Userspace mode forwards traffic arriving on
the VPC overlay to the VM's loopback network, so it does not need NET_ADMIN or /dev/net/tun.
The VM needs outbound HTTPS, DNS, and UDP access to the selected relay port. The command above
assigns an external IPv4 address. To use --no-address, provide Cloud NAT or equivalent internet
egress. No inbound rule is required for the demo service itself.
Verify the agent
Inspect the startup service if the tunnel is not ready yet:
gcloud compute ssh "$INSTANCE_NAME" \
--zone="$GCP_ZONE" \
--command='sudo systemctl status google-startup-scripts.service --no-pager'Then verify both services, the agent health endpoint, and the private backend:
gcloud compute ssh "$INSTANCE_NAME" \
--zone="$GCP_ZONE" \
--command='sudo systemctl is-active apoxy-demo-backend.service apoxy-tunnel.service; curl --fail http://127.0.0.1:8081/healthz; curl --fail http://127.0.0.1:8080/'
apoxy vpc tunnel listBoth services return active, the health endpoint reports one active connection, and the backend
returns Hello through the Apoxy GCE userspace tunnel. The tunnel list includes an agent labeled
tunnel.apoxy.dev/name=gce-private-api.
The alpha client does not yet accept a token file directly. The container reads the root-only file at startup, but the expanded token is visible in the agent process arguments to an operator with host-level access. Keep the VM access-restricted and rotate the token after a credential exposure.
Publish the private service
Create a VPCService that selects the GCE agent:
apiVersion: vpc.apoxy.dev/v1alpha1
kind: VPCService
metadata:
name: gce-private-api
spec:
networkRef:
name: default
selector:
matchLabels:
tunnel.apoxy.dev/name: gce-private-apiapoxy apply -f gce-vpc-service.yaml
apoxy vpc service get gce-private-api -o yamlContinue when Ready=True and at least one endpoint is present.
Attach a hostname to the default Gateway. Replace gce-api.example.com; for an Apoxy-managed
zone, also set spec.zone to the zone root.
apiVersion: core.apoxy.dev/v1alpha3
kind: DomainRecord
spec:
name: gce-api.example.com
tls: {}
target:
ref:
group: gateway.apoxy.dev
kind: Gateway
name: defaultapoxy apply -f gce-domain.yamlRoute port 8080 to the VPCService's internal name:
apiVersion: core.apoxy.dev/v1alpha2
kind: Backend
metadata:
name: gce-private-api
spec:
endpoints:
- fqdn: gce-private-api.default.vpc.apoxy.net
---
apiVersion: gateway.apoxy.dev/v1
kind: HTTPRoute
metadata:
name: gce-private-api
spec:
parentRefs:
- name: default
port: 443
hostnames:
- gce-api.example.com
rules:
- backendRefs:
- group: core.apoxy.dev
kind: Backend
name: gce-private-api
port: 8080apoxy apply -f gce-route.yaml
apoxy domain list \
--field-selector spec.name=gce-api.example.com \
-o yaml
curl https://gce-api.example.com/A successful request returns Hello through the Apoxy GCE userspace tunnel.
Clean up
Delete the Apoxy objects before terminating their selected agent, then remove the dedicated Google Cloud resources:
apoxy delete -f gce-route.yaml
apoxy delete -f gce-domain.yaml
apoxy delete -f gce-vpc-service.yaml
gcloud compute instances delete "$INSTANCE_NAME" \
--zone="$GCP_ZONE" \
--quiet
gcloud secrets delete "$SECRET_NAME" --quiet
gcloud iam service-accounts delete "$SERVICE_ACCOUNT_EMAIL" --quiet