labnotes.sh

> nota

VM-Series detrás de un Gateway Load Balancer en AWS

# networking# cloud# security

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.

Arquitectura de referencia en dos zonas: el virtual private gateway reparte hacia el endpoint del GWLB de cada zona, que envia en GENEVE al VM-Series de esa misma zona

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

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

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.

INSPECT ENABLE NO GWLB GENEVE 6081 VM-Series no desencapsula nada vuelve al túnel reglas en 0 hits · ningún deny · ningún log INSPECT ENABLE YES GWLB GENEVE 6081 decap → policy → re-encap App-ID ve el paquete interno GENEVE GWLB endpoint workload El health check TCP 80 atraviesa las dos rutas sin cambio: por eso el target se ve healthy en ambos casos.

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:

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

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

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

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

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

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

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

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

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

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

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

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.

Fixrequest plugins vm_series aws gwlb inspect enable yes

E2 · política

El deny final impide que el target llegue a healthy

SíntomaTarget.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.

Fixservice 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.

Causaset rulebase security rules agrega al final del rulebase.

Fixmove ... top o before <cleanup>, y verificar el orden antes de commitear.

E5 · CLI

La ruta de la zona cambia según multi-vsys

SíntomaInvalid 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.

Fixset 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íntomaInvalid 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.

Fixshow 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.


← todas las notas