Networking
Firewall rules, DNS configuration, IAP SSH access, the Cloud SQL Proxy, and secret synchronization.
Firewall Rules
Two firewall rules are defined in infra/modules/compute-vm/main.tf:
HTTP/HTTPS (public)
resource "google_compute_firewall" "allow_http_https" {
name = "trovella-${var.environment}-allow-http-https"
network = "default"
allow {
protocol = "tcp"
ports = ["80", "443"]
}
source_ranges = ["0.0.0.0/0"]
target_tags = ["trovella-web"]
}
Allows inbound HTTP and HTTPS from any IP. The VM is tagged trovella-web to match this rule. Port 80 traffic is redirected to 443 by Caddy.
IAP SSH (restricted)
resource "google_compute_firewall" "allow_iap_ssh" {
name = "trovella-${var.environment}-allow-iap-ssh"
network = "default"
allow {
protocol = "tcp"
ports = ["22"]
}
source_ranges = ["35.235.240.0/20"]
target_tags = ["iap-ssh"]
}
Allows SSH only from Google's Identity-Aware Proxy IP range (35.235.240.0/20). There is no public port 22. The VM is tagged iap-ssh to match this rule.
What Is Not Open
- No public SSH (port 22) -- brute-force attacks are impossible
- No direct database access (port 5432) -- Cloud SQL is accessed via the Auth Proxy sidecar
- No direct Typesense access (port 8108) -- internal Docker network only
- No direct Inngest access (port 8288) -- SSH tunnel only
- No ICMP/ping -- default GCP behavior, no explicit rule added
DNS Configuration
DNS is managed in Cloudflare (registrar transfer from GoDaddy pending ~mid-May 2026).
| Record | Type | Value | Proxy |
|---|---|---|---|
trovella.ai | A | VM static IP | DNS only (grey cloud) |
www.trovella.ai | A | VM static IP | DNS only (grey cloud) |
Both records point to the same VM static IP. Caddy handles the www-to-apex redirect.
DNS-only mode is required because Caddy uses HTTP-01 ACME challenges for Let's Encrypt certificate issuance. If Cloudflare proxy mode were enabled, Cloudflare would intercept the challenge requests and certificate issuance would fail.
IAP SSH Access
All administrative access to the VM goes through Identity-Aware Proxy (IAP). IAP authenticates the developer's Google account before allowing the SSH tunnel.
Direct SSH
gcloud compute ssh trovella-prod-vm \
--zone=us-central1-a --project=trovella-prod \
--tunnel-through-iap
SSH with Command
gcloud compute ssh trovella-prod-vm \
--zone=us-central1-a --project=trovella-prod \
--tunnel-through-iap \
--command="docker compose -f /opt/trovella/docker-compose.prod.yml ps"
SSH Tunnel for Admin Dashboards
# Inngest dashboard
gcloud compute ssh trovella-prod-vm \
--zone=us-central1-a --project=trovella-prod \
--tunnel-through-iap -- -L 8288:localhost:8288
# Drizzle Studio
gcloud compute ssh trovella-prod-vm \
--zone=us-central1-a --project=trovella-prod \
--tunnel-through-iap -- -L 4983:localhost:4983
SCP via IAP
The deploy pipeline uses SCP through IAP to transfer files to the VM:
gcloud compute scp \
infra/docker-compose.prod.yml \
infra/Caddyfile \
infra/sync-secrets-vm.sh \
trovella-prod-vm:~ \
--zone=us-central1-a --project=trovella-prod \
--tunnel-through-iap --quiet
IAM Requirements
IAP access requires two IAM roles:
| Role | Purpose |
|---|---|
roles/iap.tunnelResourceAccessor | Allows creating IAP tunnels to the VM |
roles/compute.instanceAdmin.v1 | Allows SSH access to Compute Engine instances |
Both are granted to the GitHub Actions service account in infra/modules/wif/main.tf for automated deploys, and to the developer's Google account for manual access.
Cloud SQL Proxy
The VM connects to Cloud SQL through the Cloud SQL Auth Proxy, running as a Docker Compose sidecar container:
web container
|
| postgresql://cloud-sql-proxy:5432/trovella
v
cloud-sql-proxy container (Docker network)
|
| IAM auth + TLS
v
Cloud SQL (trovella-prod:us-central1:trovella-prod)
How It Works
- The
cloud-sql-proxycontainer authenticates using the VM service account's IAM credentials (via the GCE metadata server) - The VM service account has
roles/cloudsql.client, which authorizes proxy connections - The proxy establishes a TLS-encrypted tunnel to Cloud SQL
- The web container connects to
cloud-sql-proxy:5432on the Docker network -- no SSL configuration needed in the application - Cloud SQL authorized networks restrict access to the VM's static IP as defense-in-depth
DATABASE_URL Rewriting
The sync-secrets-vm.sh script automatically rewrites DATABASE_URL to route through the proxy:
# The secret stores: postgresql://user:pass@<cloud-sql-public-ip>:5432/trovella
# The script rewrites to: postgresql://user:pass@cloud-sql-proxy:5432/trovella
sed -i -E 's|(DATABASE_URL="postgresql://[^@]+@)[^:/]+:[0-9]+/|\1cloud-sql-proxy:5432/|' "$TEMP_FILE"
This means the DATABASE_URL secret in GCP Secret Manager contains the direct Cloud SQL URL (with public IP), but the application always connects through the proxy.
Secret Synchronization
Application secrets are stored in GCP Secret Manager and synced to the VM at deploy time by infra/sync-secrets-vm.sh.
Flow
- CI deploy job SCPs
sync-secrets-vm.shto the VM - The script reads each secret from Secret Manager using
gcloud secrets versions access latest - Secrets are written to a temp file, then atomically moved to
/opt/trovella/.env - The
.envfile is chmod 600 (readable only by root) - Docker Compose reads the
.envfile via theenv_filedirective
Secrets Mapped
| Secret Manager ID | Environment Variable |
|---|---|
trovella-anthropic-api-key | ANTHROPIC_API_KEY |
trovella-google-ai-api-key | GOOGLE_AI_API_KEY |
trovella-database-url | DATABASE_URL |
trovella-better-auth-secret | BETTER_AUTH_SECRET |
trovella-better-auth-url | BETTER_AUTH_URL |
trovella-google-oauth-client-id | GOOGLE_CLIENT_ID |
trovella-google-oauth-client-secret | GOOGLE_CLIENT_SECRET |
trovella-resend-api-key | RESEND_API_KEY |
trovella-upstash-redis-url | REDIS_URL |
trovella-upstash-redis-token | UPSTASH_REDIS_TOKEN |
trovella-sentry-dsn | SENTRY_DSN |
trovella-inngest-event-key | INNGEST_EVENT_KEY |
trovella-inngest-signing-key | INNGEST_SIGNING_KEY |
trovella-typesense-api-key | TYPESENSE_API_KEY |
Static Variables
The script also writes non-secret configuration:
NODE_ENV=production
HOSTNAME=0.0.0.0
PORT=3000
NEXT_PUBLIC_BETTER_AUTH_URL=https://trovella.ai
TYPESENSE_URL=http://typesense:8108
INNGEST_BASE_URL=http://inngest:8288
CLOUD_SQL_CONNECTION_NAME=trovella-prod:us-central1:trovella-prod
Adding a New Secret
- Create the secret in Terraform: add to the
secretslist ininfra/environments/prod/main.tf - Set the secret value:
gcloud secrets versions add trovella-<name> --data-file=- --project=trovella-prod - Add the mapping to
infra/sync-secrets-vm.sh - Add the mapping to
scripts/sync-secrets.sh(local dev) - Add the variable to
apps/web/.env.example - If
NEXT_PUBLIC_*, also add as a build arg inapps/web/Dockerfileand thebuild-pushCI job
Workload Identity Federation
The CI/CD pipeline authenticates to GCP without long-lived service account keys using Workload Identity Federation (WIF), defined in infra/modules/wif/main.tf.
GitHub Actions exchanges an OIDC token from token.actions.githubusercontent.com for a short-lived GCP access token. The WIF pool is scoped to the specific GitHub repository via an attribute_condition. The service account (github-actions-ci) has cross-project IAM grants for deploying, reading secrets, and accessing IAP tunnels.
For details on the deploy pipeline that uses WIF, see Delivery -- Pipeline.