> nota
VM-Series detrás de un Gateway Load Balancer en AWS
Arquitectura — Qué se construye
Un Gateway Load Balancer reparte el tráfico hacia dos VM-Series, uno por zona. El firewall no rutea ni reescribe direcciones: recibe el paquete encapsulado en GENEVE, lo inspecciona y lo devuelve al mismo túnel.
La simetría por zona es deliberada. Con cross-zone load balancing desactivado, el tráfico que entra por el endpoint de una zona se inspecciona en el firewall de esa misma zona y vuelve por donde vino. Eso mantiene la sesión en un solo firewall, que es lo que un equipo con estado necesita, y evita cargos de tráfico entre zonas.

Direccionamiento de una implementación real. El nodo del GWLB y el VM-Series comparten la subnet fwdata: por eso el health check llega desde una dirección de esa misma red y es tráfico intrazona.
Direccionamiento — Cinco subnets por zona
Cada rol necesita su propia subnet porque cada una lleva una tabla de rutas distinta. Mezclarlas es la forma más rápida de crear un bucle entre el endpoint y el workload.
| Rol | Qué vive ahí | AZ a | AZ b |
|---|---|---|---|
| public | NAT gateway | 10.20.0.0/24 | 10.20.10.0/24 |
| mgmt | eth0 del VM-Series | 10.20.1.0/24 | 10.20.11.0/24 |
| fwdata | eth1 del VM-Series y los nodos del GWLB | 10.20.2.0/24 | 10.20.12.0/24 |
| gwlbe | endpoint del GWLB | 10.20.3.0/24 | 10.20.13.0/24 |
| app | workloads | 10.20.4.0/24 | 10.20.14.0/24 |
Qué ruta lleva cada tabla
-
app —
0.0.0.0/0al endpoint de su zona. Todo lo que sale del workload va a inspección. -
gwlbe —
0.0.0.0/0al NAT gateway, más propagación del virtual private gateway. Es la tabla que decide a dónde va el tráfico ya inspeccionado. -
fwdata — solo la ruta local. El GENEVE nace y muere dentro de la VPC.
-
edge del VGW — las subnets de app apuntando a su endpoint. Sin esta tabla el tráfico que llega del túnel entra directo al workload sin pasar por el firewall.
Alcance — Qué se inspecciona y qué no
Conviene decidirlo antes de escribir la política, porque no los tres caminos salen gratis.
| Camino | Cómo llega a inspección | Estado |
|---|---|---|
| Híbrido on-prem ↔ workload | Edge route table del virtual private gateway | funciona |
| Egress workload → internet | 0.0.0.0/0 de la tabla de app hacia el endpoint |
funciona |
| Este-oeste app-a ↔ app-b | Requiere rutas más específicas que la local del VPC |
ver limitaciones |
Costos — Qué cuesta tener esto encendido
Casi 3 dólares por hora, y cuatro de cada cinco se los lleva la licencia del firewall, no la infraestructura de AWS.
Tarifas de us-east-1: la infraestructura consultada contra la API de precios de AWS, el software contra la tabla de dimensiones del listing PAYG. No incluyen transferencia de datos.
| Concepto | USD/hora | × | USD/mes |
|---|---|---|---|
Licencia VM-Series — PAYG, m5.xlarge |
1,1700 | 2 | 1 708,20 |
VM-Series — cómputo m5.xlarge |
0,1920 | 2 | 280,32 |
| NAT gateway | 0,0450 | 2 | 65,70 |
| Conexión VPN site-to-site | 0,0500 | 1 | 36,50 |
Workloads t3.micro |
0,0104 | 2 | 15,18 |
| GWLB endpoint | 0,0100 | 2 | 14,60 |
| IPv4 pública en uso | 0,0050 | 4 | 14,60 |
| EBS gp3 — 2×60 GB + 2×8 GB | 0,0149 | — | 10,88 |
| Gateway Load Balancer | 0,0125 | 1 | 9,12 |
| GWLB — capacidad (LCU mínima) | 0,0040 | 1 | 2,92 |
| Total | 2,9562 | 2 158,03 |
| Si queda encendido | Costo | Composición |
|---|---|---|
| una hora | 2,96 USD | licencia 79 % · cómputo 13 % · red 8 % |
| un día | 70,95 USD | |
| un mes | 2 158 USD |
La licencia manda
El cargo de software del listing PAYG es seis veces el costo de cómputo de la misma instancia: 1,17 contra 0,192 la hora. Se factura por AWS Marketplace y por eso no aparece en la API de precios ni en las estimaciones que solo miran recursos de AWS.
El precio va por tamaño de instancia, no por familia: todas las xlarge pagan 1,17, las large 0,99, las 2xlarge 1,80 y las 4xlarge 3,69. Bajar de m5.xlarge a m5.large ahorraría 263 USD al mes en licencia — pero el GWLB necesita al menos 10.0.2 y una instancia con suficientes NIC, así que hay que verificar el soporte antes de achicar.
Qué apagar
-
Parar los dos firewalls corta el 92 % del gasto por hora — licencia más cómputo. Es la única palanca que mueve la aguja.
-
Con todo apagado siguen corriendo 0,20 USD/hora — unos 143 al mes: los NAT gateway, el GWLB, los endpoints, la VPN y las IPv4 públicas cobran por hora aunque no pase un paquete.
-
Para un lab intermitente,
terraform destroyy recrear sale mejor que apagar: son 85 recursos y el ciclo completo son minutos.
Referencia del despliegue: AMI PA-VM-AWS-11.1.15, product code e9yfvyj3uag5uo5j2hjikv74n, listing «VM-Series Next-Gen Virtual Firewall w/Advanced Threat Prevention (PAYG)».
Fase 1 — Infraestructura en AWS
terraform init
terraform plan # leerlo entero, incluidas las lineas con "-"
terraform apply
# comprobar que el direccionamiento interno del tunel quedo bien
terraform output pa440_tunnel1
terraform output pa440_tunnel2
# antes de dar por buena cualquier regla de security group
terraform plan -detailed-exitcode # 0 = sin deriva
Lea el plan completo, no solo el resumen. El Plan: N to add, M to change no distingue entre «agrega un CIDR» y «revoca los que había»: en un aws_security_group con bloques ingress inline, un cambio in-place aparece como el conjunto viejo con - y el nuevo con +.
Los parámetros que no admiten variación:
| Recurso | Valor | Por qué |
|---|---|---|
| Target group | GENEVE puerto 6081 |
Es el único protocolo que acepta un GWLB |
| Tipo de destino | ip |
Con instance se registra la interfaz primaria, que es la de gestión |
| Health check | TCP puerto 80 |
El GWLB no puede consultar un path arbitrario de PAN-OS |
| Stickiness | source_ip_dest_ip_proto |
El firewall no reescribe la 5-tupla; el flujo debe volver siempre al mismo equipo |
| ENI de datos | source_dest_check = false |
Sin esto AWS descarta el tráfico que no está dirigido a la interfaz |
| Security group | UDP 6081 y TCP 80 desde el CIDR del VPC |
GENEVE y health check |
Cuidado con el AMI
Use el parámetro público de SSM del listing al que esté suscrito. Un AMI de otro listing arranca igual pero factura contra una suscripción que no tiene. Y no lo resuelva con most_recent: en el catálogo de Palo Alto hay versiones más nuevas cuyo AMI tiene fecha anterior, así que ordenar por fecha de creación elige la versión equivocada.
Verificación
Los endpoints en Available y la edge route table asociada al virtual private gateway, con las rutas de las subnets de app apuntando a sus endpoints.
Terraform — Los recursos que importan
El despliegue completo son 85 recursos. La mayoría es plomería repetida por zona; estos son los que llevan la lógica del diseño.
| Grupo | Recursos | Detalle |
|---|---|---|
| Red base | 1 + 10 | VPC y cinco subnets por zona |
| Tablas de ruta | 8 + 11 rutas | public, mgmt, fwdata, gwlbe, app y la edge del VGW |
| Salida a internet | 1 + 2 + 2 | IGW, NAT gateway y sus EIP |
| GWLB | 5 | balanceador, listener, target group, servicio de endpoint y 2 endpoints |
| Firewalls | 2 + 4 + 2 | instancias, dos ENI cada una y las EIP de gestión |
| VPN | 4 | customer gateway, VGW, conexión y propagación de rutas |
| Seguridad | 3 | un security group por rol |
El balanceador y su target group
El GWLB vive en las subnets fwdata, junto a los firewalls. El target se registra por IP y no por instancia: con instance AWS usaría la interfaz primaria, que acá es la de gestión.
resource "aws_lb" "gwlb" {
load_balancer_type = "gateway"
subnets = [for k, v in aws_subnet.fwdata : v.id]
enable_cross_zone_load_balancing = false
}
resource "aws_lb_target_group" "fw" {
target_type = "ip"
protocol = "GENEVE"
port = 6081
health_check {
protocol = "TCP"
port = 80
}
# El firewall no reescribe la 5-tupla: el flujo debe volver
# siempre a la misma instancia.
stickiness {
type = "source_ip_dest_ip_proto"
enabled = true
}
}
resource "aws_lb_target_group_attachment" "fw" {
for_each = local.subnets
target_id = aws_network_interface.fw_data[each.key].private_ip
availability_zone = local.az[each.key]
port = 6081
}
La asociación de borde
Es la pieza que hace que el tráfico entrante se inspeccione. Una tabla de rutas asociada al virtual private gateway con gateway_id, no a una subnet, y con rutas más específicas que la local del VPC.
resource "aws_route" "vgw_edge_to_app" {
for_each = local.subnets
route_table_id = aws_route_table.vgw_edge.id
destination_cidr_block = each.value.app # 10.20.4.0/24 y 10.20.14.0/24
vpc_endpoint_id = aws_vpc_endpoint.gwlbe[each.key].id
}
resource "aws_route_table_association" "vgw_edge" {
gateway_id = aws_vpn_gateway.vgw.id # asociacion de BORDE
route_table_id = aws_route_table.vgw_edge.id
}
Las interfaces del firewall
Dos NIC: gestión en device_index 0 y datos en el 1, que es la que el VM-Series ve como ethernet1/1. El source_dest_check va en false solo en la de datos.
resource "aws_network_interface" "fw_data" {
for_each = local.subnets
subnet_id = aws_subnet.fwdata[each.key].id
security_groups = [aws_security_group.fwdata.id]
source_dest_check = false
}
resource "aws_instance" "fw" {
for_each = local.subnets
instance_type = "m5.xlarge"
network_interface { network_interface_id = ...fw_mgmt[each.key].id, device_index = 0 }
network_interface { network_interface_id = ...fw_data[each.key].id, device_index = 1 }
user_data = <<-EOT
type=dhcp-client
hostname=fw-gwlb-${each.key}
dhcp-accept-server-hostname=no
dhcp-accept-server-domain=no
EOT
}
La selección del AMI
Con parámetro público de SSM, nunca con most_recent: en el catálogo de Palo Alto hay versiones más nuevas cuyo AMI tiene fecha anterior, así que ordenar por fecha de creación elige la versión equivocada.
data "aws_ssm_parameter" "panos" {
name = "/aws/service/marketplace/prod-<listing>/pan-os-<version>"
}
Los tres security groups
| Grupo | Ingress | Desde |
|---|---|---|
fwdata |
UDP 6081, TCP 80, ICMP | CIDR del VPC |
mgmt |
TCP 22, TCP 443, ICMP | lista de administración + prefijos on-prem |
app |
todo | CIDR del VPC + prefijos on-prem |
El de gestión toma una lista de CIDR, no un string. Con un solo valor la única salida es agregar la IP real a mano en la consola, y como los bloques ingress son inline, el siguiente apply la revoca en silencio.
Código — La configuración completa
Los siete archivos tal cual se aplican, con las direcciones públicas y los identificadores de cuenta sustituidos por marcadores. Es el despliegue entero: no hay módulos externos ni estado compartido.
| Archivo | Contenido |
|---|---|
00-variables.tf |
Proveedor, variables y el mapa de subnets por zona. |
01-vpc.tf |
VPC, las diez subnets, IGW, NAT gateway y los tres security groups. |
02-gwlb.tf |
Balanceador, target group, listener, servicio de endpoint y endpoints. |
03-firewalls.tf |
AMI por SSM, las ENI, los VM-Series y los workloads. |
04-vpn.tf |
Customer gateway, VGW y la conexión con sus dos túneles. |
05-routing.tf |
Las seis tablas de rutas, incluida la edge asociada al VGW. |
06-outputs.tf |
Salidas, entre ellas el direccionamiento interno de los túneles. |
terraform {
required_version = ">= 1.5"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
random = { source = "hashicorp/random", version = "~> 3.6" }
}
}
provider "aws" {
region = var.region
default_tags {
tags = {
Project = "kf-gwlb-lab"
ManagedBy = "terraform"
Owner = "csegovia"
}
}
}
variable "region" {
type = string
default = "us-east-1"
}
variable "vpc_cidr" {
description = "Verificado libre contra la tabla de rutas del PA-410."
type = string
default = "10.20.0.0/16"
}
variable "azs" {
type = list(string)
default = ["us-east-1a", "us-east-1b"]
}
# Cinco subnets por AZ. Cada una tiene un rol distinto en el path de GENEVE.
locals {
az = { a = var.azs[0], b = var.azs[1] }
subnets = {
a = {
public = "10.20.0.0/24" # NAT GW
mgmt = "10.20.1.0/24" # eth0 del VM-Series
fwdata = "10.20.2.0/24" # eth1, GENEVE, target del GWLB
gwlbe = "10.20.3.0/24" # endpoints del GWLB
app = "10.20.4.0/24" # workloads
}
b = {
public = "10.20.10.0/24"
mgmt = "10.20.11.0/24"
fwdata = "10.20.12.0/24"
gwlbe = "10.20.13.0/24"
app = "10.20.14.0/24"
}
}
# Rutas app -> GWLBE para cada prefijo del lab, por AZ.
app_lab_routes = {
for p in setproduct(keys(local.subnets), var.lab_cidrs) :
"${p[0]}|${p[1]}" => { az = p[0], cidr = p[1] }
}
}
# --- VPN / BGP ---------------------------------------------------------------
variable "cgw_public_ip" {
description = "IP publica del PA-410 (ethernet1/1)."
type = string
default = "<IP-PUBLICA-CGW>"
}
variable "cgw_asn" {
description = "AS local del PA-410, ya existente en el VR default."
type = number
default = 65000
}
variable "vgw_asn" {
type = number
default = 64512
}
variable "tunnel1_inside_cidr" {
description = "169.254.100.0/30 NO se puede: la usa el peer HEX-CASA."
type = string
default = "169.254.200.0/30"
}
variable "tunnel2_inside_cidr" {
type = string
default = "169.254.201.0/30"
}
variable "lab_cidrs" {
description = "Prefijos del home lab que se alcanzan por el tunel y se inspeccionan."
type = list(string)
default = [
"192.168.100.0/24",
"192.168.106.0/24",
"10.1.0.0/16",
"10.142.0.0/16",
]
}
# --- Firewalls ---------------------------------------------------------------
# Parametro publico de SSM que publica AWS Marketplace. Resuelve al AMI
# correcto por region sin hardcodear IDs.
#
# prod-<ID-DEL-LISTING> = listing "VM-Series Next-Gen Virtual Firewall
# w/Advanced Threat Prevention (PAYG)"
#
# OJO: usar el AMI de OTRO listing hace que la instancia facture contra una
# suscripcion que no tiene. El product code va atado al listing.
variable "panos_ssm_parameter" {
type = string
default = "/aws/service/marketplace/prod-<ID-DEL-LISTING>/pan-os-11.1.15"
}
# Override manual. Si se setea, gana sobre el parametro SSM.
variable "panos_ami_id" {
type = string
default = ""
}
variable "panos_instance_type" {
description = "GWLB necesita minimo 10.0.2. m5.xlarge soporta las NICs necesarias."
type = string
default = "m5.xlarge"
}
variable "key_pair_name" {
type = string
}
# Lista, no string: la IP publica de salida del equipo desde el que se
# administra NO siempre es la de ethernet1/1 del PA-410. Con un solo valor la
# unica salida era agregar la IP real a mano en la consola, y como los bloques
# ingress son inline, el siguiente terraform apply la revocaba en silencio.
variable "admin_cidrs" {
description = "CIDRs con acceso de gestion a los VM-Series (SSH, HTTPS, ICMP)."
type = list(string)
default = ["<IP-PUBLICA-CGW>/32"]
}
resource "aws_vpc" "lab" {
cidr_block = var.vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = { Name = "kf-gwlb-vpc" }
}
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.lab.id
tags = { Name = "kf-gwlb-igw" }
}
# --- Subnets -----------------------------------------------------------------
resource "aws_subnet" "public" {
for_each = local.subnets
vpc_id = aws_vpc.lab.id
cidr_block = each.value.public
availability_zone = local.az[each.key]
tags = { Name = "kf-public-${each.key}" }
}
resource "aws_subnet" "mgmt" {
for_each = local.subnets
vpc_id = aws_vpc.lab.id
cidr_block = each.value.mgmt
availability_zone = local.az[each.key]
tags = { Name = "kf-mgmt-${each.key}" }
}
resource "aws_subnet" "fwdata" {
for_each = local.subnets
vpc_id = aws_vpc.lab.id
cidr_block = each.value.fwdata
availability_zone = local.az[each.key]
tags = { Name = "kf-fwdata-${each.key}" }
}
resource "aws_subnet" "gwlbe" {
for_each = local.subnets
vpc_id = aws_vpc.lab.id
cidr_block = each.value.gwlbe
availability_zone = local.az[each.key]
tags = { Name = "kf-gwlbe-${each.key}" }
}
resource "aws_subnet" "app" {
for_each = local.subnets
vpc_id = aws_vpc.lab.id
cidr_block = each.value.app
availability_zone = local.az[each.key]
tags = { Name = "kf-app-${each.key}" }
}
# --- NAT Gateway por AZ ------------------------------------------------------
# Uno por AZ. Con uno solo compartido el retorno cruza AZ y el path se vuelve
# dificil de explicar. Son US$0.045/hr cada uno: el segundo item mas caro
# despues de los firewalls.
resource "aws_eip" "nat" {
for_each = local.subnets
domain = "vpc"
tags = { Name = "kf-eip-nat-${each.key}" }
depends_on = [aws_internet_gateway.igw]
}
resource "aws_nat_gateway" "nat" {
for_each = local.subnets
allocation_id = aws_eip.nat[each.key].id
subnet_id = aws_subnet.public[each.key].id
tags = { Name = "kf-nat-${each.key}" }
depends_on = [aws_internet_gateway.igw]
}
# --- Security groups ---------------------------------------------------------
resource "aws_security_group" "mgmt" {
name = "kf-sg-mgmt"
description = "Gestion de los VM-Series"
vpc_id = aws_vpc.lab.id
ingress {
description = "HTTPS GUI"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = concat(var.admin_cidrs, var.lab_cidrs)
}
ingress {
description = "SSH"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = concat(var.admin_cidrs, var.lab_cidrs)
}
ingress {
description = "ICMP"
from_port = -1
to_port = -1
protocol = "icmp"
cidr_blocks = concat(var.admin_cidrs, var.lab_cidrs)
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = { Name = "kf-sg-mgmt" }
}
# GOTCHA GWLB #1: si falta el 6081 el target queda unhealthy y AWS no dice
# por que. Es el error mas comun de toda la integracion.
resource "aws_security_group" "fwdata" {
name = "kf-sg-fwdata"
description = "Interfaz de datos: GENEVE + health check del GWLB"
vpc_id = aws_vpc.lab.id
ingress {
description = "GENEVE desde el GWLB"
from_port = 6081
to_port = 6081
protocol = "udp"
cidr_blocks = [var.vpc_cidr]
}
ingress {
description = "Health check del target group"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = [var.vpc_cidr]
}
ingress {
from_port = -1
to_port = -1
protocol = "icmp"
cidr_blocks = [var.vpc_cidr]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = { Name = "kf-sg-fwdata" }
}
resource "aws_security_group" "app" {
name = "kf-sg-app"
description = "Workloads"
vpc_id = aws_vpc.lab.id
ingress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = concat([var.vpc_cidr], var.lab_cidrs)
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = { Name = "kf-sg-app" }
}
# =============================================================================
# Gateway Load Balancer
# El GWLB encapsula en GENEVE (UDP 6081) y reparte hacia los VM-Series.
# Los firewalls trabajan como bump-in-the-wire: no rutean, no hacen NAT.
# =============================================================================
resource "aws_lb" "gwlb" {
name = "kf-gwlb"
load_balancer_type = "gateway"
subnets = [for k, v in aws_subnet.fwdata : v.id]
# Probado en true (2026-08-17) para descartar que fuera la causa de que el
# GWLB no entregue al target: NO cambio nada, el trafico sigue muriendo entre
# el endpoint y el target. Se vuelve a false, que es el diseno buscado
# (inspeccion local por AZ) y evita cargos de trafico cross-AZ.
enable_cross_zone_load_balancing = false
tags = { Name = "kf-gwlb" }
}
# GOTCHA GWLB #2: el health check no puede pegarle a un path cualquiera del
# PAN-OS. Se usa TCP:80 y hay que habilitar HTTP en el interface management
# profile de la interfaz de datos, o el target nunca pasa a healthy.
resource "aws_lb_target_group" "fw" {
name = "kf-gwlb-tg"
target_type = "ip"
protocol = "GENEVE"
port = 6081
vpc_id = aws_vpc.lab.id
health_check {
protocol = "TCP"
port = 80
interval = 10
healthy_threshold = 3
unhealthy_threshold = 3
}
# El firewall no reescribe la 5-tupla, asi que el flujo debe volver siempre
# a la misma instancia.
stickiness {
type = "source_ip_dest_ip_proto"
enabled = true
}
tags = { Name = "kf-gwlb-tg" }
}
# Se registra la IP de la ENI de datos, no el instance-id: con target_type
# "instance" el GWLB usaria la interfaz primaria, que aca es la de gestion.
resource "aws_lb_target_group_attachment" "fw" {
for_each = local.subnets
target_group_arn = aws_lb_target_group.fw.arn
target_id = aws_network_interface.fw_data[each.key].private_ip
availability_zone = local.az[each.key]
port = 6081
}
resource "aws_lb_listener" "gwlb" {
load_balancer_arn = aws_lb.gwlb.arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.fw.arn
}
}
# --- Endpoint service + endpoints por AZ -------------------------------------
resource "aws_vpc_endpoint_service" "gwlb" {
acceptance_required = false
gateway_load_balancer_arns = [aws_lb.gwlb.arn]
tags = { Name = "kf-gwlb-endpoint-service" }
}
resource "aws_vpc_endpoint" "gwlbe" {
for_each = local.subnets
vpc_id = aws_vpc.lab.id
service_name = aws_vpc_endpoint_service.gwlb.service_name
vpc_endpoint_type = "GatewayLoadBalancer"
subnet_ids = [aws_subnet.gwlbe[each.key].id]
tags = { Name = "kf-gwlbe-${each.key}" }
}
# No se usa data "aws_ami" con most_recent: en la lista de AMIs de Palo Alto
# la 11.1.15 es una release MAS nueva que la 11.1.6-h35, pero su AMI tiene
# fecha ANTERIOR. Ordenar por CreationDate elige la version equivocada.
data "aws_ssm_parameter" "panos" {
count = var.panos_ami_id == "" ? 1 : 0
name = var.panos_ssm_parameter
}
locals {
panos_ami = var.panos_ami_id != "" ? var.panos_ami_id : data.aws_ssm_parameter.panos[0].value
}
# Dos NICs: gestion y datos. Sin overlay routing, sin NAT, sin interfaz
# untrust. Todo el trafico entra y sale encapsulado en GENEVE por eth1.
resource "aws_network_interface" "fw_mgmt" {
for_each = local.subnets
subnet_id = aws_subnet.mgmt[each.key].id
security_groups = [aws_security_group.mgmt.id]
tags = { Name = "kf-fw-${each.key}-mgmt" }
}
resource "aws_network_interface" "fw_data" {
for_each = local.subnets
subnet_id = aws_subnet.fwdata[each.key].id
security_groups = [aws_security_group.fwdata.id]
source_dest_check = false
tags = { Name = "kf-fw-${each.key}-data" }
}
resource "aws_eip" "fw_mgmt" {
for_each = local.subnets
domain = "vpc"
network_interface = aws_network_interface.fw_mgmt[each.key].id
tags = { Name = "kf-eip-fw-${each.key}-mgmt" }
depends_on = [aws_internet_gateway.igw]
}
resource "aws_instance" "fw" {
for_each = local.subnets
ami = local.panos_ami
instance_type = var.panos_instance_type
key_name = var.key_pair_name
network_interface {
network_interface_id = aws_network_interface.fw_mgmt[each.key].id
device_index = 0
}
network_interface {
network_interface_id = aws_network_interface.fw_data[each.key].id
device_index = 1
}
user_data = <<-EOT
type=dhcp-client
hostname=kf-fw-gwlb-${each.key}
dns-primary=169.254.169.253
dns-secondary=8.8.8.8
dhcp-accept-server-hostname=no
dhcp-accept-server-domain=no
EOT
root_block_device {
volume_size = 60
volume_type = "gp3"
}
tags = { Name = "kf-fw-gwlb-${each.key}" }
}
# --- Workloads ---------------------------------------------------------------
data "aws_ami" "al2023" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-2023.*-x86_64"]
}
}
# Sin IP publica. Se prueban desde el home lab por el tunel, que es
# justamente el escenario que se quiere demostrar.
resource "aws_instance" "app" {
for_each = local.subnets
ami = data.aws_ami.al2023.id
instance_type = "t3.micro"
subnet_id = aws_subnet.app[each.key].id
private_ip = cidrhost(local.subnets[each.key].app, 10)
vpc_security_group_ids = [aws_security_group.app.id]
key_name = var.key_pair_name
user_data = <<-EOT
#!/bin/bash
dnf install -y nginx
echo "kf-gwlb-lab · stack ${each.key} · $(hostname)" > /usr/share/nginx/html/index.html
systemctl enable --now nginx
EOT
tags = { Name = "kf-app-${each.key}" }
}
# PSK: 8-64 chars, alfanumerico mas . y _ , no puede empezar con 0.
resource "random_password" "psk" {
count = 2
length = 32
special = true
override_special = "._"
numeric = true
upper = true
lower = true
}
resource "aws_customer_gateway" "cgw" {
bgp_asn = var.cgw_asn
ip_address = var.cgw_public_ip
type = "ipsec.1"
tags = { Name = "kf-cgw-pa410" }
}
resource "aws_vpn_gateway" "vgw" {
vpc_id = aws_vpc.lab.id
amazon_side_asn = var.vgw_asn
tags = { Name = "kf-vgw" }
}
resource "aws_vpn_connection" "s2s" {
customer_gateway_id = aws_customer_gateway.cgw.id
vpn_gateway_id = aws_vpn_gateway.vgw.id
type = "ipsec.1"
static_routes_only = false # BGP
# --- Tunel 1 ---------------------------------------------------------------
tunnel1_inside_cidr = var.tunnel1_inside_cidr
tunnel1_preshared_key = random_password.psk[0].result
tunnel1_ike_versions = ["ikev2"]
tunnel1_phase1_encryption_algorithms = ["AES256"]
tunnel1_phase1_integrity_algorithms = ["SHA2-256"]
tunnel1_phase1_dh_group_numbers = [14]
tunnel1_phase1_lifetime_seconds = 28800
tunnel1_phase2_encryption_algorithms = ["AES256"]
tunnel1_phase2_integrity_algorithms = ["SHA2-256"]
tunnel1_phase2_dh_group_numbers = [14]
tunnel1_phase2_lifetime_seconds = 3600
tunnel1_startup_action = "start" # AWS inicia; tenemos IP fija
tunnel1_dpd_timeout_action = "restart"
tunnel1_dpd_timeout_seconds = 30
# --- Tunel 2 ---------------------------------------------------------------
tunnel2_inside_cidr = var.tunnel2_inside_cidr
tunnel2_preshared_key = random_password.psk[1].result
tunnel2_ike_versions = ["ikev2"]
tunnel2_phase1_encryption_algorithms = ["AES256"]
tunnel2_phase1_integrity_algorithms = ["SHA2-256"]
tunnel2_phase1_dh_group_numbers = [14]
tunnel2_phase1_lifetime_seconds = 28800
tunnel2_phase2_encryption_algorithms = ["AES256"]
tunnel2_phase2_integrity_algorithms = ["SHA2-256"]
tunnel2_phase2_dh_group_numbers = [14]
tunnel2_phase2_lifetime_seconds = 3600
tunnel2_startup_action = "start"
tunnel2_dpd_timeout_action = "restart"
tunnel2_dpd_timeout_seconds = 30
tags = { Name = "kf-vpn-pa410" }
}
# =============================================================================
# ROUTING
#
# El path completo de un flujo de egress:
# app -> GWLBE (misma AZ) -> GWLB -> firewall -> GWLB -> GWLBE -> NAT GW -> IGW
#
# El path de un flujo hibrido entrante:
# home lab -> VGW -> [edge route table] -> GWLBE -> firewall -> GWLBE -> app
# =============================================================================
# --- Public: NAT GW + IGW ----------------------------------------------------
# El retorno del NAT GW tiene que volver por el firewall, si no el flujo queda
# asimetrico y solo se inspecciona la ida.
resource "aws_route_table" "public" {
for_each = local.subnets
vpc_id = aws_vpc.lab.id
tags = { Name = "kf-rt-public-${each.key}" }
}
resource "aws_route" "public_default" {
for_each = aws_route_table.public
route_table_id = each.value.id
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
resource "aws_route" "public_return_to_fw" {
for_each = local.subnets
route_table_id = aws_route_table.public[each.key].id
destination_cidr_block = each.value.app
vpc_endpoint_id = aws_vpc_endpoint.gwlbe[each.key].id
}
resource "aws_route_table_association" "public" {
for_each = aws_subnet.public
subnet_id = each.value.id
route_table_id = aws_route_table.public[each.key].id
}
# --- Mgmt --------------------------------------------------------------------
# Trafico de gestion, no se inspecciona a proposito: si el firewall se cae,
# igual se puede llegar a administrarlo.
resource "aws_route_table" "mgmt" {
vpc_id = aws_vpc.lab.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
tags = { Name = "kf-rt-mgmt" }
}
resource "aws_route_table_association" "mgmt" {
for_each = aws_subnet.mgmt
subnet_id = each.value.id
route_table_id = aws_route_table.mgmt.id
}
# --- Fwdata ------------------------------------------------------------------
# Solo la ruta local. El GENEVE nace y muere dentro de la VPC.
resource "aws_route_table" "fwdata" {
vpc_id = aws_vpc.lab.id
tags = { Name = "kf-rt-fwdata" }
}
resource "aws_route_table_association" "fwdata" {
for_each = aws_subnet.fwdata
subnet_id = each.value.id
route_table_id = aws_route_table.fwdata.id
}
# --- GWLBE: salida despues de inspeccion -------------------------------------
# Aca cae el trafico ya inspeccionado que devuelve el firewall.
resource "aws_route_table" "gwlbe" {
for_each = local.subnets
vpc_id = aws_vpc.lab.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.nat[each.key].id
}
tags = { Name = "kf-rt-gwlbe-${each.key}" }
}
resource "aws_route_table_association" "gwlbe" {
for_each = aws_subnet.gwlbe
subnet_id = each.value.id
route_table_id = aws_route_table.gwlbe[each.key].id
}
# Los prefijos del lab llegan por BGP. GOTCHA #1 del post anterior sigue
# vigente: la propagacion viene apagada por defecto.
resource "aws_vpn_gateway_route_propagation" "gwlbe" {
for_each = aws_route_table.gwlbe
vpn_gateway_id = aws_vpn_gateway.vgw.id
route_table_id = each.value.id
}
resource "aws_vpn_gateway_route_propagation" "mgmt" {
vpn_gateway_id = aws_vpn_gateway.vgw.id
route_table_id = aws_route_table.mgmt.id
}
# --- App: todo sale por el GWLBE ---------------------------------------------
# Sin propagacion del VGW aca: si se propagaran los prefijos del lab, el
# trafico iria directo al VGW sin pasar por el firewall.
resource "aws_route_table" "app" {
for_each = local.subnets
vpc_id = aws_vpc.lab.id
tags = { Name = "kf-rt-app-${each.key}" }
}
resource "aws_route" "app_default" {
for_each = local.subnets
route_table_id = aws_route_table.app[each.key].id
destination_cidr_block = "0.0.0.0/0"
vpc_endpoint_id = aws_vpc_endpoint.gwlbe[each.key].id
}
resource "aws_route" "app_to_lab" {
for_each = local.app_lab_routes
route_table_id = aws_route_table.app[each.value.az].id
destination_cidr_block = each.value.cidr
vpc_endpoint_id = aws_vpc_endpoint.gwlbe[each.value.az].id
}
resource "aws_route_table_association" "app" {
for_each = aws_subnet.app
subnet_id = each.value.id
route_table_id = aws_route_table.app[each.key].id
}
# --- Edge route table del VGW ------------------------------------------------
# Esto es lo que hace que el trafico hibrido se inspeccione. Sin esta
# asociacion el trafico del tunel entra directo a las subnets app.
resource "aws_route_table" "vgw_edge" {
vpc_id = aws_vpc.lab.id
tags = { Name = "kf-rt-vgw-edge" }
}
resource "aws_route" "vgw_edge_to_app" {
for_each = local.subnets
route_table_id = aws_route_table.vgw_edge.id
destination_cidr_block = each.value.app
vpc_endpoint_id = aws_vpc_endpoint.gwlbe[each.key].id
}
resource "aws_route_table_association" "vgw_edge" {
gateway_id = aws_vpn_gateway.vgw.id
route_table_id = aws_route_table.vgw_edge.id
}
output "firewalls" {
description = "EIP de gestion de cada VM-Series y la IP de datos registrada en el target group."
value = {
for k in keys(local.subnets) : k => {
mgmt_eip = aws_eip.fw_mgmt[k].public_ip
data_ip = aws_network_interface.fw_data[k].private_ip
}
}
}
output "app_servers" {
description = "Workloads. Sin IP publica: se prueban desde el home lab por el tunel."
value = { for k, v in aws_instance.app : k => v.private_ip }
}
output "gwlb_endpoints" {
value = { for k, v in aws_vpc_endpoint.gwlbe : k => v.id }
}
output "target_group_arn" {
description = "Para vigilar el estado con: aws elbv2 describe-target-health --target-group-arn ..."
value = aws_lb_target_group.fw.arn
}
# --- Datos para armar el tunel en el PA-410 ---------------------------------
# inside_local / inside_remote salen de los atributos que devuelve AWS, no de
# cidrhost(). AWS asigna el PRIMER host del /30 al VGW y el segundo al customer
# gateway; calcularlo a mano invita a dejarlo al reves, que es justo lo que
# paso antes: el PA quedaba con la .1 (que es del VGW) y peereaba contra la .2.
output "pa440_tunnel1" {
value = {
aws_peer_ip = aws_vpn_connection.s2s.tunnel1_address
inside_local = aws_vpn_connection.s2s.tunnel1_cgw_inside_address
inside_remote = aws_vpn_connection.s2s.tunnel1_vgw_inside_address
bgp_peer_asn = var.vgw_asn
}
}
output "pa440_tunnel2" {
value = {
aws_peer_ip = aws_vpn_connection.s2s.tunnel2_address
inside_local = aws_vpn_connection.s2s.tunnel2_cgw_inside_address
inside_remote = aws_vpn_connection.s2s.tunnel2_vgw_inside_address
bgp_peer_asn = var.vgw_asn
}
}
# CREDENCIAL VIVA. Nunca en el blog ni en screenshots.
output "psk_tunnel1" {
value = aws_vpn_connection.s2s.tunnel1_preshared_key
sensitive = true
}
output "psk_tunnel2" {
value = aws_vpn_connection.s2s.tunnel2_preshared_key
sensitive = true
}
Fase 2 — Configuración del VM-Series
Idéntica en los dos equipos: la interfaz de datos toma su IP por DHCP, así que no hay nada que personalizar por zona.
configure
set network profiles interface-management-profile GWLB-HC http yes
set network profiles interface-management-profile GWLB-HC ping yes
set network interface ethernet ethernet1/1 layer3 dhcp-client enable yes
set network interface ethernet ethernet1/1 layer3 dhcp-client create-default-route no
set network interface ethernet ethernet1/1 layer3 interface-management-profile GWLB-HC
set network virtual-router default interface ethernet1/1
set zone gwlb network layer3 ethernet1/1
El http yes del management profile es lo que hace que el firewall responda el health check. La ruta de la zona depende de multi-vsys: con off es set zone; con on es set vsys vsys1 zone. Verificalo con show system info | match multi-vsys antes de escribir la línea: si la zona no se crea, después fallan todas las reglas que la referencian.
Política: una sola zona, todo intrazona
El tráfico entra y sale por la misma interfaz, así que origen y destino caen en la misma zona. La política tiene que permitirlo explícitamente.
El health check necesita su propia regla
Habilitar HTTP en el management profile es necesario pero no suficiente. El health check sale de un nodo del GWLB en la misma subnet hacia la IP de datos del firewall: es tráfico intrazona y pasa por la política. Cualquier regla de cleanup any/any lo mata antes, y el target queda unhealthy para siempre sin que AWS explique nada.
set service TCP-80 protocol tcp port 80
set address-group GWLB-NODES static [ FWDATA-A FWDATA-B ]
set rulebase security rules GWLB-HEALTHCHECK from gwlb
set rulebase security rules GWLB-HEALTHCHECK to gwlb
set rulebase security rules GWLB-HEALTHCHECK source GWLB-NODES
set rulebase security rules GWLB-HEALTHCHECK destination any
set rulebase security rules GWLB-HEALTHCHECK application any
set rulebase security rules GWLB-HEALTHCHECK service TCP-80
set rulebase security rules GWLB-HEALTHCHECK action allow
move rulebase security rules GWLB-HEALTHCHECK top
El move no es cosmético: las reglas nuevas se agregan al final, o sea debajo del cleanup, donde nunca matchean.
Para las reglas de inspección use service any. La combinación application any con service application-default no matchea nada: sin aplicaciones concretas en la regla no hay puerto default del cual derivar el servicio.
Verificación
Los dos targets en healthy. Tarda unos 30 segundos con el intervalo por defecto.
aws elbv2 describe-target-health --region <region> \
--target-group-arn <arn>
Fase 3 — Habilitar el parsing GENEVE
Este es el paso que hace que el diseño funcione, y el único que no produce ningún error si se lo salta.
PAN-OS no desencapsula el GENEVE hasta que se lo pide. Sin eso recibe los paquetes UDP 6081, no ve las direcciones internas, no inspecciona nada y nunca los devuelve al túnel.
En los dos equipos
request plugins vm_series aws gwlb inspect enable yes
request plugins vm_series aws gwlb overlay-routing enable no
show plugins vm_series aws gwlb
GWLB enabled tiene que decir True. El mensaje vpc-endpoint association not found es normal en bump-in-the-wire: esa asociación pertenece al modo overlay routing, que acá se descarta a propósito.
El health check no depende del parsing, y por eso el tablero de AWS se ve sano mientras no se inspecciona un solo paquete. Es la razón de que este error sea tan difícil de encontrar.
Verificación final
Que el workload responda prueba que el tráfico pasa, no que se inspeccione. La prueba real está en el log: la aplicación identificada tiene que ser la real y no not-applicable.
show rule-hit-count vsys vsys-name vsys1 rule-base security rules all
show log traffic rule equal <REGLA>
web-browsing gwlb 49880 <origen>
HIBRIDO-IN allow gwlb 80 <workload>
tcp-fin
No use direction equal backward: devuelve las entradas más viejas del log, y con los health checks corriendo cada pocos segundos nunca verá ahí su prueba.
BGPEl plano de control del lado híbrido
Solo aplica si además termina una VPN contra la misma arquitectura. Salida real del equipo de borde, con el identificador del router y el peer preexistente sustituidos.
Una sola pantalla con todo
El summary es el comando que más rinde: en una vista da el estado de cada sesión y cuántos prefijos entran y salen por cada una.
admin@fw-borde> show routing protocol bgp summary
==========
router id: <router-id>
virtual router: default
Local AS: 65000
Install BGP routes: yes
Graceful Restart: supported
Default local preference: 100
mp-bgp-enable: yes
afi-safi-ipv4-unicast: yes
rib-out entries: current 140, peak 142
peer PEER-PREEXISTENTE: AS 65010, Established, IP <inside-peer>
bgpAfiIpv4/unicast pfx: Accepted pfx: 1, Advertised pfx: 54
peer AWS-VGW-1: AS 64512, Established, IP 169.254.200.1
bgpAfiIpv4/unicast pfx: Accepted pfx: 3, Advertised pfx: 43
peer AWS-VGW-2: AS 64512, Established, IP 169.254.201.1
bgpAfiIpv4/unicast pfx: Accepted pfx: 3, Advertised pfx: 43
Lo que hay que leer acá: los dos peers de AWS en Established, 3 prefijos aceptados de cada uno —el VPC y sus dos subnets de app— y 43 anunciados hacia cada uno. Que los dos túneles muestren números idénticos es la señal de que la redundancia está activa y no solo configurada.
El peer que ya existía sigue con su propio contador: Advertised pfx: 54. Si al agregar el grupo nuevo ese número cambia, tocó una política que era de otro peer.
El detalle de una sesión
admin@fw-borde> show routing protocol bgp peer
==========
Peer: AWS-VGW-2 (id 5)
virtual router: default
Peer router id: 169.254.201.1
Remote AS: 64512
Peer group: AWS (id 1)
Peer status: Established, for 20118 seconds
Passive: no
Multi-hop TTL: 1
Remote Address: 169.254.201.1:179
Local Address: 169.254.201.2:40411
Prefix limit: 100
Holdtime: 30 (config 30)
Keep-Alive interval: 10 (config 10)
Update messages: in 4, out 5
Total messages: in 2018, out 2318
Last error:
Flap counts: 1, established 1 times
Nexthop set to self: no
----------
remove private AS number: no
----------
Capability: Multiprotocol Extensions(1) value: IPv4 Unicast
Capability: Route Refresh(yes)
Capability: 4-Byte AS Number(65) value: 64512
----------
Prefix counter for: bgpAfiIpv4 / unicast
Incoming Prefix: Accepted 3, Rejected 0, Policy Rej 0, Total 3
Outgoing Prefix: 43
Advertised Prefix: 43
Cuatro líneas que conviene mirar y que suelen pasarse por alto:
-
Local Address: 169.254.201.2contraRemote Address: 169.254.201.1— la local es la.2. Invertido, el túnel levanta igual y la sesión nunca establece. -
remove private AS number: no— con todos los ASN privados, quitarlos del AS_PATH elimina la detección de bucles. -
Flap counts: 1, established 1 times— estableció una vez y no volvió a caer. Un contador que sube apunta a MTU o a inestabilidad del túnel. -
Accepted 3, Rejected 0, Policy Rej 0— separa «no llegan rutas» de «llegan y las descarta una política de import».
Lo que queda en la tabla de reenvío
admin@fw-borde> show routing route destination 10.20.0.0/16
flags: A:active, ?:loose, C:connect, H:host, S:static, ~:internal, R:rip, O:ospf, B:bgp,
Oi:ospf intra-area, Oo:ospf inter-area, O1:ospf ext-type-1, O2:ospf ext-type-2, E:ecmp, M:multicast
VIRTUAL ROUTER: default (id 1)
==========
destination nexthop metric flags age next-AS
10.20.0.0/16 169.254.201.1 100 A?B 20117 64512
10.20.4.0/24 169.254.201.1 100 A?B 20117 64512
10.20.14.0/24 169.254.201.1 100 A?B 20117 64512
total routes shown: 3
Los tres prefijos entran por los dos túneles, pero en la FIB queda uno solo: el de mejor camino, con flags A?B —activo, origen incompleto, aprendido por BGP—. Si ese túnel cae, el otro toma el relevo sin intervención. Para ver las seis entradas antes de la selección hay que mirar la tabla BGP con show routing protocol bgp loc-rib.
Si el firewall anuncia y AWS no acepta
Cuando el summary muestra prefijos anunciados pero la consola de AWS reporta cero rutas aceptadas, el sospechoso es export-nexthop: con resolve, las rutas reaprendidas de un tercer peer pueden salir con un next-hop que AWS descarta. use-self lo corrige.
Verificación — Cómo se ve cuando está bien
Capturas de una implementación funcionando. Las dos que más importan son la asociación de borde y el log con la aplicación identificada: la primera explica por qué el tráfico entra a inspección, la segunda prueba que efectivamente se inspecciona.
En la consola de AWS
Lo que tiene que reportar el plano de control.
Target group · pestaña de destinos

Los dos targets en Healthy, uno por zona, ambos en puerto 6081. Cuidado: esta pantalla se ve exactamente igual con la inspección funcionando y sin ella, porque el health check es TCP 80 y no depende del parsing GENEVE. Que estén verdes no prueba que se inspeccione nada.
Edge route table · rutas

Las dos rutas que desvían el tráfico entrante a inspección: cada subnet de app apuntando al endpoint de su zona. Son más específicas que la local del VPC, que es lo que las hace ganar.
Edge route table · asociación de borde

La pieza que suele faltar: la tabla asociada al virtual private gateway. Sin esta asociación las rutas de la captura anterior no se evalúan y el tráfico del túnel entra directo al workload sin pasar por el firewall.
Endpoints del Gateway Load Balancer

Uno por zona, los dos en Available. Cada uno vive en la subnet gwlbe de su zona.
Conexión VPN · detalles del túnel

Si además conecta on-prem: los dos túneles arriba y con rutas BGP aceptadas. Es la fuente autoritativa del estado del BGP, por encima de lo que reporte el firewall.
Instancias del despliegue

Cuatro instancias: dos VM-Series como firewalls y dos workloads, repartidos entre las dos zonas.
En los VM-Series
Lo que confirma que hay inspección y no solo tránsito.
Policies · Security

El orden correcto del rulebase. La regla del health check va primera; si queda debajo del deny final, el target nunca llega a healthy. Los contadores confirman qué se está usando: el tráfico híbrido y el egress suman hits, el este-oeste queda en cero porque en este diseño no pasa por el firewall.
Monitor · Traffic filtrado por regla

La verificación que importa. La columna Application dice web-browsing, no not-applicable: App-ID está viendo el paquete interno, o sea el decap GENEVE funciona. Si ahí aparece not-applicable, el tráfico pasa pero no se inspecciona.
Network · Interfaces

La interfaz de datos en Layer3, con su virtual router, la zona de inspección y el management profile aplicado. La IP la asigna DHCP y tiene que coincidir con el target registrado en el grupo.
En el equipo de borde
Solo si además termina una VPN contra la misma arquitectura.
IPSec Tunnels en el equipo de borde

Los dos túneles hacia AWS en verde, cada uno sobre su interfaz de túnel. Las filas en blanco son túneles de producción del mismo equipo, ocultos a propósito.
BGP · peers

Los dos peers de AWS en Established, con la local en la .2 y el peer en la .1 de cada /30. Arriba, un peer preexistente con su propio uptime: al agregar el peer group nuevo, conviene confirmar que los que ya estaban no flapearon.
Errores — Diez que no dan mensaje
Todos aparecieron construyendo esta arquitectura. Ninguno produce un error que apunte a la causa.
E1 · GWLB
El parsing GENEVE no viene habilitado
Síntoma — Todo se ve sano y no se inspecciona un solo paquete. Sin errores.
Causa — PAN-OS no desencapsula el UDP 6081 hasta que se lo pide. overlay-routing es un ajuste distinto.
Fix — request plugins vm_series aws gwlb inspect enable yes
E2 · política
El deny final impide que el target llegue a healthy
Síntoma — Target.Timeout permanente, aunque el management profile tenga HTTP.
Causa — El health check es intrazona y pasa por la política, donde el deny final matchea primero.
Fix — Regla de allow para TCP 80 desde las subnets de datos, en la primera posición.
E3 · política
application any con application-default no matchea nada
Síntoma — Reglas de inspección en 0 hits desde el día uno, sin explicación.
Causa — Sin aplicaciones concretas no hay puerto default del cual derivar el servicio.
Fix — service any, o listar aplicaciones explícitas.
E4 · política
Las reglas nuevas nacen debajo del deny final
Síntoma — Se agrega una regla correcta y no matchea nunca.
Causa — set rulebase security rules agrega al final del rulebase.
Fix — move ... top o before <cleanup>, y verificar el orden antes de commitear.
E5 · CLI
La ruta de la zona cambia según multi-vsys
Síntoma — Invalid syntax al crear la zona, y después fallos en cascada por not a valid reference.
Causa — Con multi-vsys off no existe el nodo vsys en la raíz.
Fix — set zone con multi-vsys off; set vsys vsys1 zone con multi-vsys on.
E6 · híbrido
AWS da el primer host del /30 al gateway, no al cliente
Síntoma — IPsec levanta y BGP nunca sale de Connect.
Causa — En el /30 interno del túnel, la .1 es del virtual private gateway y la .2 del customer gateway.
Fix — Leer los atributos que devuelve AWS en vez de calcular las direcciones a mano.
E7 · híbrido
Una regla any/any final también mata el tráfico intrazona
Síntoma — BGP no establece sobre las /30 del túnel.
Causa — Una regla from any to any es de tipo universal: matchea intrazona además de interzona.
Fix — Regla intrazona explícita para las IP internas del túnel, por encima del deny.
E8 · Terraform
Los ingress inline revocan lo que agregues a mano
Síntoma — Se pierde el acceso de gestión después de un apply que no tocaba ese recurso.
Causa — Con bloques inline, Terraform es autoritativo sobre el security group entero.
Fix — Que la variable de acceso sea una lista y que las IP vivan en el código. Verificar con terraform plan -detailed-exitcode.
E9 · CLI
Un PTY de 80 columnas corrompe los comandos largos
Síntoma — Invalid syntax intermitente en comandos que son correctos.
Causa — El CLI wrapea insertando un espacio en el medio: ethernet1/1 llega como ethern et1/1.
Fix — Agrandar el PTY antes del spawn, o bajar con edit <nodo> para acortar cada línea.
E10 · logs
direction equal backward muestra lo más viejo
Síntoma — El tráfico de prueba no aparece en el log aunque la regla sume hits.
Causa — Devuelve las entradas más antiguas, y el log está dominado por health checks.
Fix — show log traffic rule equal <NOMBRE>
Limitaciones — Lo que este diseño no resuelve
El este-oeste entre subnets de app no se inspecciona
En la tabla de rutas de cada subnet de app, la ruta local del VPC le gana por longest-prefix match al default hacia el endpoint. El tráfico entre workloads va directo, y la regla que lo contempla queda en cero hits.
Agregar rutas más específicas que la local no alcanza: cada dirección entraría por el endpoint de su propia zona y sería inspeccionada por un firewall distinto. Con un equipo con estado eso no funciona, y el stickiness no lo salva porque al invertirse origen y destino el hash cambia. Las salidas reales son mandar el este-oeste por un solo endpoint —perdiendo independencia entre zonas— o aceptar que ese camino no se inspecciona y sacar la regla para no prometer algo que no ocurre.
Sin MSS clamp en las interfaces de túnel
si además termina una VPN contra el mismo equipo, tenga en cuenta que adjust-tcp-mss no existe bajo tunnel units en PAN-OS 11.1: el nodo solo aparece en subinterfaces ethernet. Sin clamp el ping pasa y las transferencias TCP grandes se cuelgan. La alternativa es clamear en las interfaces LAN de ingreso, a costa de afectar todo su tráfico.
Los workloads dependen de la inspección para aprovisionarse
Su salida a internet pasa por el endpoint, el GWLB y el firewall. Mientras la inspección no funcione no tienen internet: cloud-init no alcanza el metadata service y el user_data nunca corre, sin ningún error visible en la consola. Por eso la prueba de extremo a extremo va al final del procedimiento y no en el medio.