Network translation
Agent-driven EKS to GKE migrations. PSO-grade outcomes, no PSO required.
npx -y skills add geoffsdesk/portage --skill network-translationAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Translate EKS network constructs to GKE equivalents. Maps ALBs/NLBs to GKE Gateways and Services, AWS Load Balancer Controller annotations to Gateway/Service annotations, Route 53 records to Cloud DNS, security groups to VPC firewall rules and NetworkPolicy, and AWS WAF rules to Cloud Armor policies. Produces translated manifests plus a network design diff. Use after gke-landing-zone, when "translate our ingress to GKE", "convert ALB ingresses to Gateway API", or "what's the network plan for the migration".
SKILL.md
22.9 KB, as published. Nobody here has run it
Network Translation
You translate EKS networking — load balancers, ingress, services, security groups, DNS, WAF — into the GKE equivalents. You produce diff'd manifests and a network design document.
Purpose
Every Service, Ingress, ALB/NLB, Route 53 record, security group, and WAF rule that crosses traffic into the EKS cluster needs a GKE counterpart. This skill produces that mapping with auditable manifests.
When to use this skill
- Phase 2, after
gke-landing-zone. - The user asks to "translate ingress", "set up GKE Gateway equivalents", "design the GKE network".
Do NOT use to translate identity (that's identity-translation) or to translate workloads (that's workload-translation).
Prerequisites
01-discovery/inventory.jsonwith the network section populated.03-landing-zone/plan.mdwith VPCs, subnets, and clusters defined.gcloudCLI;kubectlcontext for both source and target clusters.
Procedure
Step 1 — Build the source-side network model
From inventory.json, enumerate per cluster:
- ALBs: ARN, originating Ingress, hostnames (
spec.rules[].host), TLS cert ARN, target groups, target type (instance/ip), listener rules, attached WAF. - NLBs: ARN, originating Service, hostnames (Route 53), target groups, port mappings.
- Internal vs internet-facing.
- VPC ID and subnets the LBs live in.
- Security groups attached to LBs and to nodes.
- Route 53 records pointing at LB DNS names (and which hosted zones).
- AWS WAF web ACLs and their rules.
Step 2 — Map LBs to GKE constructs
Default mapping:
| EKS source | GKE target |
|---|---|
| Internet-facing ALB (HTTP/S, with host/path rules) | Gateway of class gke-l7-global-external-managed (or gke-l7-regional-external-managed) plus one or more HTTPRoutes |
| Internal ALB | Gateway of class gke-l7-rilb (regional internal) |
| Internet-facing NLB (TCP/UDP) | Service type: LoadBalancer with cloud.google.com/l4-rbs: "enabled" annotation (regional external pass-through) or gke-l4-global-external-managed Gateway |
| Internal NLB | Service type: LoadBalancer with networking.gke.io/load-balancer-type: "Internal" |
Ingress with kubernetes.io/ingress.class: alb | HTTPRoute attached to a Gateway |
service.beta.kubernetes.io/aws-load-balancer-type: external | drop annotation; pick Gateway or LB Service per type |
Produce one Gateway per (LB, scope) tuple. Multiple Ingresses fronting the same ALB merge into multiple HTTPRoutes attached to the same Gateway.
Example translation:
Source (EKS):
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web
namespace: storefront
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-east-1:...:certificate/...
alb.ingress.kubernetes.io/wafv2-acl-arn: arn:aws:wafv2:us-east-1:...:regional/webacl/...
alb.ingress.kubernetes.io/healthcheck-path: /healthz
spec:
rules:
- host: shop.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service: { name: web, port: { number: 80 } }
Target (GKE):
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: storefront-external
namespace: storefront
annotations:
networking.gke.io/certmap: storefront-cert-map # if using Certificate Manager
spec:
gatewayClassName: gke-l7-global-external-managed
listeners:
- name: https
protocol: HTTPS
port: 443
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: shop-example-com-tls # OR use Google-managed cert via certmap
allowedRoutes:
namespaces: { from: Same }
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: web
namespace: storefront
spec:
parentRefs:
- name: storefront-external
hostnames: [ "shop.example.com" ]
rules:
- matches:
- path: { type: PathPrefix, value: / }
backendRefs:
- name: web
port: 80
---
apiVersion: networking.gke.io/v1
kind: HealthCheckPolicy
metadata:
name: web
namespace: storefront
spec:
default:
config:
type: HTTP
httpHealthCheck:
port: 80
requestPath: /healthz
targetRef:
group: ""
kind: Service
name: web
---
apiVersion: networking.gke.io/v1
kind: GCPBackendPolicy
metadata:
name: web
namespace: storefront
spec:
default:
securityPolicy: shop-armor # Cloud Armor policy
targetRef:
group: ""
kind: Service
name: web
Step 3 — TLS / certificates
Decide per hostname:
- Google-managed certificate (Certificate Manager) — preferred for any hostname you control via Cloud DNS or you can prove with DNS01.
- Self-managed certificate — when the cert needs to come from your CA, or when ACME issuance is centralized elsewhere.
Plan the migration of every ACM-issued cert: usually re-issue in Certificate Manager rather than export/import (private keys can't always be exported).
Step 4 — Cloud Armor (replaces AWS WAF)
No Google-published AWS WAF → Cloud Armor mapping exists. Both Cloud Armor preconfigured WAF rules and Cloud Armor rule tuning reference OWASP CRS lineage, not AWS Managed Rule Groups. The mappings below are tool-opinionated heuristics derived from the OWASP coverage of each side. Validate against real traffic; do not present them to a customer as Google guidance.
Step 4.1 — Pick the CRS version
Cloud Armor publishes preconfigured rules from three CRS versions:
| CRS | Suffix | Notes |
|---|---|---|
| 4.22 | -v422-stable, -v422-canary | Recommended by Google. The nodejs rule from v3.3 was reclassified as generic in v4.22 (same 934-prefixed IDs). |
| 3.3 | -v33-stable, -v33-canary | Has separate java and nodejs rules. Useful for JSON-SQLi pre-filters (see Step 4.4). |
| 3.0 | -stable, -canary (no version suffix) | Legacy. Avoid for new policies. |
Default to v4.22. Choose canary for new tunings; promote to stable after a soak.
Step 4.2 — Map AWS Managed Rule Groups (heuristic)
| AWS WAF managed group | Closest Cloud Armor preconfigured rules | Notes |
|---|---|---|
AWSManagedRulesCommonRuleSet | xss-v422-stable, lfi-v422-stable, protocolattack-v422-stable, scannerdetection-v422-stable, methodenforcement-v422-stable | Common ruleset spans many categories; combine multiples |
AWSManagedRulesKnownBadInputsRuleSet | protocolattack-v422-stable, scannerdetection-v422-stable | Known-bad inputs and scanners |
AWSManagedRulesSQLiRuleSet | sqli-v422-stable (+ json-sqli-canary if JSON bodies) | See JSON-SQLi note in Step 4.4 |
AWSManagedRulesPHPRuleSet | php-v422-stable | |
AWSManagedRulesUnixRuleSet | rce-v422-stable, lfi-v422-stable | RCE + LFI cover Unix attack surface |
AWSManagedRulesWindowsRuleSet | rce-v422-stable | No Windows-specific group; RCE covers OS injection |
AWSManagedRulesLinuxRuleSet | rce-v422-stable, lfi-v422-stable | |
AWSManagedRulesAnonymousIpList | (No 1:1) — use Cloud Armor origin.region_code + IP allow-lists | Heuristic; consider Cloud Armor Adaptive Protection |
AWSManagedRulesAmazonIpReputationList | (No 1:1) — Adaptive Protection + custom CEL deny | |
AWSManagedRulesBotControlRuleSet | reCAPTCHA Enterprise integration with Cloud Armor | Different UX; surface as design decision |
| Custom regex / string-match rules | CEL expression(s) on the security policy | Translate each rule individually |
| Rate-based rules | rate_limit_options (action rate_based_ban or throttle) | Not a preconfigured-rule expression |
Surface this mapping in the network-design doc explicitly as "heuristic — review per workload."
Step 4.3 — Tuning level (paranoia)
Sensitivity is the OWASP CRS paranoia level: 0–4. Lower = higher confidence, fewer false positives. Default is 4 (all signatures evaluated). Selecting N opts in all signatures with sensitivity <= N.
CEL syntax:
evaluatePreconfiguredWaf('sqli-v422-stable', {'sensitivity': 1})
Production starting points (tool-opinionated):
xss-v422-stableat sensitivity 2.sqli-v422-stableat sensitivity 2; also enablejson-sqli-canaryif API accepts JSON bodies.lfi-v422-stable,rfi-v422-stable,rce-v422-stableat sensitivity 2.protocolattack-v422-stableat sensitivity 2.methodenforcement-v422-stableat sensitivity 1.scannerdetection-v422-stableat sensitivity 1 (3+ produces noise).
Opt-out specific noisy signatures with opt_out_rule_ids rather than dropping the whole rule:
evaluatePreconfiguredWaf('sqli-v422-stable', {
'sensitivity': 4,
'opt_out_rule_ids': ['owasp-crs-v042200-id942350-sqli', 'owasp-crs-v042200-id942360-sqli']
})
The signature-ID prefix must match the CRS version of the rule set (v042200 for v4.22, v030301 for v3.3, v030001 for v3.0).
Step 4.4 — JSON-SQLi (Cloud Armor's known WAF-bypass)
Bodies must be JSON-parsed for SQLi to be caught in JSON. Google's recommended pattern is to use both:
evaluatePreconfiguredWaf('json-sqli-canary', {'sensitivity':0, 'opt_in_rule_ids':['owasp-crs-id942550-sqli']})
||
evaluatePreconfiguredWaf('sqli-v33-stable', {'sensitivity': 2})
If your application accepts JSON request bodies and any SQL-touching backend, both go in.
Step 4.5 — CVE rules and cve-canary
cve-canary ships canary-only and is opt-in by signature ID. Log4j and React RCE coverage:
| Signature ID | Sensitivity | What it catches |
|---|---|---|
owasp-crs-v030001-id044228-cve | 1 | Base rule for CVE-2021-44228 / CVE-2021-45046 (Log4j) |
owasp-crs-v030001-id144228-cve | 1 | Google enhancements for bypass / obfuscation |
owasp-crs-v030001-id244228-cve | 3 | Higher-sensitivity bypass detection (some FPs) |
owasp-crs-v030001-id344228-cve | 3 | Base64-encoded bypass detection (some FPs) |
google-mrs-v202512-id000001-rce / google-mrs-v202512-id000002-rce | n/a | React RCE; gate behind a request pre-filter (below) |
React RCE recommended pre-filter (verbatim):
(has(request.headers['next-action']) || has(request.headers['rsc-action-id']) ||
request.headers['content-type'].contains('multipart/form-data') ||
request.headers['content-type'].contains('application/x-www-form-urlencoded'))
&& evaluatePreconfiguredWaf('cve-canary', {'sensitivity': 0,
'opt_in_rule_ids': ['google-mrs-v202512-id000001-rce','google-mrs-v202512-id000002-rce']})
Step 4.6 — Terraform skeleton
resource "google_compute_security_policy" "shop_armor" {
project = var.cluster_project
name = "shop-armor"
# OWASP CRS v4.22 baseline
rule {
action = "deny(403)"
priority = 1000
match { expr { expression = "evaluatePreconfiguredWaf('sqli-v422-stable', {'sensitivity': 2})" } }
}
rule {
action = "deny(403)"
priority = 1010
match { expr { expression = "evaluatePreconfiguredWaf('xss-v422-stable', {'sensitivity': 2})" } }
}
rule {
action = "deny(403)"
priority = 1020
match { expr { expression = "evaluatePreconfiguredWaf('protocolattack-v422-stable', {'sensitivity': 2})" } }
}
rule {
action = "deny(403)"
priority = 1030
match { expr { expression = "evaluatePreconfiguredWaf('rce-v422-stable', {'sensitivity': 2})" } }
}
rule {
action = "deny(403)"
priority = 1040
match { expr { expression = "evaluatePreconfiguredWaf('lfi-v422-stable', {'sensitivity': 2})" } }
}
# Rate limiting (per-IP throttle)
rule {
action = "rate_based_ban"
priority = 2000
rate_limit_options {
rate_limit_threshold { count = 200, interval_sec = 60 }
enforce_on_key = "IP"
ban_duration_sec = 600
conform_action = "allow"
exceed_action = "deny(429)"
}
match {
versioned_expr = "SRC_IPS_V1"
config { src_ip_ranges = ["*"] }
}
}
# Default allow
rule {
action = "allow"
priority = 2147483647
match { versioned_expr = "SRC_IPS_V1" config { src_ip_ranges = ["*"] } }
description = "default rule"
}
}
Attach via GCPBackendPolicy.spec.default.securityPolicy on the relevant Service.
Step 4.7 — Operational gotchas Google calls out
- Rule changes take "several minutes to propagate." Plan accordingly.
- Cloud Armor evaluates preconfigured rules against the first 64 KB of request body. Larger payloads are not inspected end-to-end.
- JSON parsing must be enabled to inspect JSON bodies.
- The
--request-*-to-excludecontrols (header / cookie / query / URI) cannot be used on rules with actionallow. - Adaptive Protection is enabled at the security-policy level (
--enable-layer7-ddos-defense); it doesn't expose a per-rule CEL expression but ML-suggests rules in alerts.
Step 5 — DNS
For every Route 53 record that points at an LB DNS name:
- Plan a Cloud DNS record in the target zone (or the same zone, depending on TLD ownership). For apex hostnames, GKE Gateway gives you an IP; use an
Arecord. For subdomains, you canCNAMEto the Gateway address — but you'll need a deterministic Gateway hostname or static IP. - For weighted DNS (used by
traffic-cutover), use Cloud DNS routing policies (weighted_round_robin) or move the records to a CDN/global LB that supports weighted backends natively. - If the existing zone is in Route 53 and you can't move it yet, plan delegation via NS records, or configure the cutover to update Route 53 directly during
traffic-cutover.
Capture every DNS change as a dns-plan.md under the run directory.
Step 6 — Security groups → VPC firewall + NetworkPolicy
EKS security groups attached to nodes and LBs map to:
- Node-to-node traffic: GKE handles intra-cluster networking via the VPC subnet's firewall rules. The Shared VPC's hierarchical firewall plus
gke-cluster-name-rules cover most. - External-to-LB: GFE health check ranges and your allow-listed CIDRs become VPC firewall
INGRESSrules targeting the LB's network tag. - Pod-to-pod within cluster: implement as Kubernetes NetworkPolicy (Dataplane V2 enforces). Translate any
aws-load-balancer-controllerannotation that restricts pod ingress into a NetworkPolicy.
Produce per-namespace NetworkPolicy YAML restricting pod ingress to known ServiceAccount selectors and ports. Default-deny in production namespaces is recommended.
Step 7 — Egress
EKS pods typically egressed via NAT Gateway with no per-pod identity. GKE pods egress via Cloud NAT, with Workload Identity providing per-workload identity for GCP API calls.
For external (non-GCP) egress destinations:
- Static egress IPs: reserve in Cloud NAT and document.
- Allow-listed destinations on the partner side: send the partner the new IPs ahead of cutover.
- VPC peering / TGW reachability: re-create with VPC peering or NCC (Network Connectivity Center).
Step 8 — Output the design diff
Render 04-network-translation/network-design.md with:
- Diagram (ASCII or Mermaid) of source vs target network topologies, side by side.
- Table mapping every source LB ARN to the target Gateway/Service.
- Cert migration plan.
- Cloud Armor policy mapping table.
- DNS cutover plan (which records, which TTLs, planned change windows).
- Firewall rule changes.
Render 04-network-translation/manifests/ with one file per Gateway and HTTPRoute, plus per-namespace NetworkPolicy.
Decision points
| Decision | Default | When to deviate |
|---|---|---|
| Gateway API vs Ingress | Gateway API | Ingress only if a controller you depend on doesn't yet support Gateway |
| Global vs regional Gateway | Global for true multi-region traffic; regional otherwise | Regional for cost or sovereignty constraints |
| ACM cert reuse vs Certificate Manager re-issue | Certificate Manager re-issue | Reuse only if private key export is feasible and required |
| AWS WAF preconfigured rules → Cloud Armor preconfigured | 1:1 by category | Custom rewrites for org-specific rules |
| NAT egress vs per-workload Cloud NAT routes | Single Cloud NAT with manual port allocation | Per-egress-destination Cloud NAT only for very high egress workloads |
Outputs / Deliverables
04-network-translation/
├── network-design.md
├── manifests/
│ ├── gateways/
│ ├── httproutes/
│ ├── network-policies/
│ ├── healthcheck-policies/
│ └── backend-policies/
├── terraform/
│ ├── cloud-armor/
│ ├── certificates/
│ └── dns/
├── dns-plan.md
└── escalations.md
Validation
- Every ALB/NLB in
inventory.jsonhas a target Gateway/Service in the manifests. - Every host in any source Ingress is covered by exactly one target HTTPRoute.
- Every TLS cert ARN in source has a planned Certificate Manager entry or self-managed cert.
- Cloud Armor mapping has been reviewed by the user; non-trivial WAF rules have explicit acceptance.
- Apply manifests to a non-prod cluster and confirm:
kubectl describe gatewayshowsProgrammed: True; the Gateway has an external IP;curl https://...succeeds against a backend that already exists or against a placeholder. - NetworkPolicy doesn't break existing flows in the soak test.
Escalation triggers
- Workloads using
LoadBalancerSourceRangeswith overlapping or conflicting CIDRs across namespaces — surface for review. - TLS certs that cannot be reissued (private CA chain, customer-supplied) — surface for human plan.
- Custom AWS WAF JS challenge / CAPTCHA flows — Cloud Armor reCAPTCHA Enterprise has a different UX; needs product decision.
- Egress dependencies on AWS-only endpoints (e.g., S3 VPC endpoint) — needs cross-cloud egress plan.
Common pitfalls
- Forgetting health check paths. The ALB health check path is in an annotation; in GKE it's a
HealthCheckPolicy. Skip and your backends look unhealthy. - Not setting
BackendConfig/GCPBackendPolicy. Timeouts default to 30s. Many workloads need longer. Translate explicitly. - TLS hostname mismatches. ACM SANs migrate cleanly; ACM wildcards plus exotic SANs sometimes don't. Verify each cert.
- Cloud Armor preconfigured WAF tuning level.
cve-canaryis sensitive;cve-stableis the production default. Match severity. - Default-deny NetworkPolicy applied with no allow rules. The cluster is now silent. Stage default-deny via a soak window.
- GKE ManagedCertificate stays in
FAILED_NOT_VISIBLEuntil LB IP attaches and DNS resolves to it. Sequence the cutover so DNS is updated before the cert is expected to validate, not after. See LFF-26. - ManagedCertificate doesn't work with nginx-ingress. Decision point: Gateway+ManagedCertificate, or nginx-ingress+cert-manager. Make it up-front. See LFF-27.
- Gateway API on GKE expects a pre-shared SslCertificate, not a ManagedCertificate CR. When targeting Gateway API, plan TLS via Certificate Manager from the start. See LFF-28.
- ALB
group.namesemantics don't translate to Gateway API. Rebuild as one Gateway with multiple HTTPRoutes. See LFF-20. - Istio Gateway static IP port collisions. Each Istio Gateway creates its own LB Service; shared static IPs collide on port 15021. See LFF-21.
- AWS LBC CRD vs Gateway API mismatch. AWS LBC v3.0.0 moved to CRDs; many EKS-only fields do not fit Gateway API directly without restructuring. See LFF-22.
- Confluent live CNI migration. Migrating CNI (to Cilium) live across clouds requires strict cluster-by-cluster sequencing. See LFF-30.
References
- Canonical sources: reference/sources.md.
- Cloud Armor preconfigured WAF rules — verbatim source for rule names and CRS versions.
- Cloud Armor rule tuning — sensitivity model (paranoia 0–4).
- GKE Gateway controller.
- Choosing a load balancer.
- Certificate Manager.
- Cloud DNS routing policies.
- docs/glossary.md — networking translations.
- reference/api-translation.md — annotation-by-annotation map.
- traffic-cutover — uses the DNS plan output.