Skip to content

Public Website Cloner — Guia de Operações

Stack: cloner · Namespace web-agents · Porta 8081
Versão: v3.6.0+

Scope status (post-Scope-Freeze 2026-05-10) — See ARCHITECTURE.md for the canonical 37 MÓDULOs + 7 Test Kinds + DOM/CPOS/PIE-PA safety architecture. ADRs 0014, 0019-0025 cover post-Freeze additions. Contato: agallon@cisco.com


Objetivo

O Public Website Cloner é um componente opcional do TLSStress.Art que permite baixar sites públicos da internet e servi-los localmente dentro do lab, sem dependência de conectividade externa durante os testes.

Isso permite que a frota de agentes browser engine e synthetic-load engine teste conteúdo real de sites conhecidos (ex.: portais bancários, e-commerce, streaming) diretamente através do NGFW em modo DUT, sem tráfego de produção saindo pela rede de testes.

Fluxo típico de uso: 1. Administrador seleciona um site público (ex.: https://example.com) e uma persona (ex.: shop) 2. O cloner baixa o site completo via interface ISP (VLAN 40 — acesso direto à internet) 3. O conteúdo clonado fica disponível em http://clone-serve:8081/shop/ 4. O Dashboard cria um target http://clone-serve:8081/shop/ na frota de teste 5. browser-engine/synthetic-load acessa o conteúdo clonado através do NGFW — exercitando inspeção TLS, regras e políticas sobre conteúdo real


Como funciona

Arquitetura e fluxo de dados

 ┌──────────────────────────────────────────────────────────────────────┐
 │  Cloner pod (1 réplica, node role=infra / UCS-4)                     │
 │                                                                      │
 │  eth0 ─── OOBI (K8s default) ─── Dashboard API ─── PostgreSQL       │
 │  net1 ─── VLAN 40 (ISP) ─────── Nexus 9000 ─── Upstream router      │
 │                                                       │              │
 │                                                    INTERNET          │
 │  ┌───────────────────────────────────────────┐        │              │
 │  │ browser engine headful + stealth   │◄───────┘              │
 │  │  headless: false  (contorna anti-bot)     │                       │
 │  │  user-agent realista, viewport real       │ ← baixa via VLAN 40  │
 │  │  navigator.webdriver = undefined          │                       │
 │  └─────────────────────┬─────────────────────┘                       │
 │                         │ escreve                                    │
 │                         ▼                                            │
 │                   /mnt/cloned/{persona}/                             │
 │                   (PVC NFS RWX — cloned-sites, OOBI only)            │
 │                                                                      │
 │  ┌───────────────────────────────────────────┐                       │
 │  │ clone-serve HTTP Server (:8081)           │                       │
 │  │  GET /healthz     → liveness probe        │                       │
 │  │  GET /metrics     → Prometheus            │                       │
 │  │  GET /{persona}/… → assets clonados       │                       │
 │  └───────────────────────────────────────────┘                       │
 │                                                                      │
 │  ┌───────────────────────────────────────────┐                       │
 │  │ Internet Health Monitor (10 s)            │                       │
 │  │  ping 8.8.8.8       → internet OK?        │                       │
 │  │  ping 1.1.1.1       → internet OK?        │                       │
 │  │  ping <gw DHCP>     → gateway VLAN 40 OK? │                       │
 │  └───────────────────────────────────────────┘                       │
 └──────────────────────────────────────────────────────────────────────┘

Ciclo de vida de um job

Admin cria job (POST /api/clone/jobs)
        │
        ▼ clone_jobs.status = 'pending'
        │
        ▼ cloner poll (GET /api/clone/agents/{id}/job — SELECT … SKIP LOCKED)
clone_jobs.status = 'running'
        │
        ▼ browser engine baixa o site via VLAN 40
escreve /mnt/cloned/{persona}/index.html, *.css, *.js, imagens…
        │
        ▼ PATCH /api/clone/jobs/{id}
clone_jobs.status = 'completed' | 'failed'
        │
        ▼
Assets servidos em http://clone-serve.web-agents.svc.cluster.local:8081/{persona}/

Rede — VLAN 40 (ISP)

Mapa de VLANs do lab

VLAN Nome Interface host Subnet Finalidade
20 dut-pw eth1.20 172.16.0.0/16 Agentes browser engine
30 dut-k6 eth1.30 172.17.0.0/16 Agentes synthetic-load engine
40 cloner-isp eth1.40 DHCP (ISP) Cloner — egresso internet
99 dut-mgmt eth1.99 10.254.254.0/24 SNMP / gerência NGFW
101–120 persona-N eth1.10N 10.1.N.0/27 Webservers de persona individuais (Synthetic)
200–209 clone-persona-N eth1.20N 10.2.N.0/27 Webservers de persona clonadas (Cloned)

VLAN 40 — características: - Sem IP fixo no host — o pod recebe IP via DHCP do roteador upstream - O Nexus 9000 deve permitir VLAN 40 no trunk do UCS-4 (role=infra) - A VLAN 40 deve ser roteada até o link ISP / upstream router do lab - Não passa pelo NGFW — acesso direto à internet sem inspeção TLS

Roteamento dentro do pod

O initContainer (routing-init, Alpine com NET_ADMIN) configura policy routing:

Routing table 100 (ISP):   default via <gateway VLAN 40> dev net1
iptables mangle OUTPUT:
  -d 10.0.0.0/8     → RETURN  (K8s/OOBI: usa eth0)
  -d 172.16.0.0/12  → RETURN  (K8s/OOBI: usa eth0)
  -d 192.168.0.0/16 → RETURN  (K8s/OOBI: usa eth0)
  -j MARK --set-mark 100       (todo o resto → usa net1/VLAN 40)

ip rule: fwmark 100 → table 100

Todo o tráfego público (TCP 80/443, UDP 443/QUIC, UDP 53/DNS, ICMP) sai via VLAN 40 automaticamente.

Pré-requisito Nexus 9000

! No Nexus 9000 — permitir VLAN 40 na porta trunk de UCS-4
interface Ethernet1/<N>
  switchport trunk allowed vlan add 40

vlan 40
  name cloner-isp-egress

HTTPS e certificados

Acesso a sites HTTPS públicos

O cloner acessa sites diretamente via VLAN 40, sem passar pelo NGFW. Portanto:

  • Os sites servem seus próprios certificados TLS (assinados por CAs públicas)
  • O Chromium valida os certificados usando o bundle Mozilla/NSS do Debian (ca-certificates)
  • Nenhuma configuração adicional é necessária para sites HTTPS públicos (Let's Encrypt, DigiCert, Sectigo, etc.)
  • A CA do NGFW não é injetada no cloner — não é necessária (o cloner bypassa o NGFW)

Fluxo TLS no cloner vs. no restante do lab

Agentes browser-engine/synthetic-load (TLS leg 1) → NGFW (TLS leg 2) → Caddy persona
  [NGFW inspeciona e re-assina com CA própria]

Cloner → VLAN 40 → Internet → site real
  [TLS direto; CA pública; sem inspeção; ca-certificates Debian]

Certificados privados ou auto-assinados

Se o site alvo usar CA privada:

# 1. Exportar a CA em formato PEM
# 2. Criar ConfigMap com a CA
kubectl create configmap cloner-extra-cas \
  --from-file=extra-ca.crt=/caminho/para/ca.pem \
  -n web-agents

# 3. Montar no pod e configurar NODE_EXTRA_CA_CERTS em 81-cloner-deployment.yaml

DNS forçado

O pod usa dnsPolicy: None. Os resolvers são fixos independentemente do DHCP:

Prioridade Servidor Finalidade
1 8.8.8.8 Google Public DNS — nomes de internet
2 208.67.222.222 OpenDNS — fallback internet
3 10.96.0.10 k3s CoreDNS — dashboard, pgbouncer, etc.

Queries para 8.8.8.8/208.67.222.222 são endereços públicos → fwmarked → saem via VLAN 40.

CoreDNS em cluster diferente do k3s padrão:

kubectl get svc kube-dns -n kube-system -o jsonpath='{.spec.clusterIP}'
# Atualizar k8s/81-cloner-deployment.yaml → dnsConfig.nameservers[2]


Deploy

Kubernetes — passo a passo

# 1. VLAN 40 no Nexus 9000 (ver seção acima)

# 2. Criar subinterface eth1.40 no UCS-4 (automático via k8s-install.sh)
sudo ip link add link eth1 name eth1.40 type vlan id 40 2>/dev/null || true
sudo ip link set eth1.40 up

# 3. Criar secrets (inclui cloner-secrets automaticamente)
sudo ./scripts/secrets-init.sh --hostname agents.mylab.local

# 4. Aplicar manifests
kubectl apply -f k8s/

# 5. Verificar
kubectl get pod -n web-agents -l app.kubernetes.io/name=cloner -o wide
kubectl logs -n web-agents -l app.kubernetes.io/name=cloner -f
# Aguardar: "[health] ping 8.8.8.8: ok X.Xms"

Docker Compose (desenvolvimento)

# Control stack deve estar em execução
export CONTROLLER_TOKEN=<mesmo que AGENT_API_TOKEN>
docker compose -f docker-compose.cloner.yml up -d

# Verificar DNS forçado
docker exec -it <cloner-container> cat /etc/resolv.conf
# Deve mostrar: nameserver 8.8.8.8 / 208.67.222.222

Gerenciar jobs

Criar job (curl)

curl -s -X POST https://agents.mylab.local/api/clone/jobs \
  -H "Authorization: Basic $(echo -n 'admin:<PASS>' | base64)" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com","personaName":"shop"}' | python3 -m json.tool

Listar / detalhar / cancelar

BASE=https://agents.mylab.local
H="Authorization: Basic $(echo -n 'admin:<PASS>' | base64)"

curl -s "$BASE/api/clone/jobs"          -H "$H" | python3 -m json.tool
curl -s "$BASE/api/clone/jobs/<job-id>" -H "$H"
curl -s -X DELETE "$BASE/api/clone/jobs/<job-id>" -H "$H"

Acessar conteúdo clonado

kubectl port-forward -n web-agents svc/clone-serve 8081:8081
# Abrir: http://localhost:8081/shop/

Monitoramento

Grafana — linha "Cloner — Internet Health & Jobs"

Painel Métrica Verde =
Acesso Internet (ISP) cloner_internet_any_up Qualquer ping respondeu
Ping 8.8.8.8 cloner_internet_up{target="8.8.8.8"} Acessível
Ping 1.1.1.1 cloner_internet_up{target="1.1.1.1"} Acessível
Gateway ISP cloner_gateway_up{gateway="<IP>"} Gateway DHCP responde ICMP
Ping RTT cloner_ping_rtt_ms Latência < 50 ms

Métricas diretas

kubectl port-forward -n web-agents svc/clone-serve 8081:8081
curl http://localhost:8081/metrics | grep cloner_

Saída esperada:

cloner_internet_up{target="8.8.8.8"} 1
cloner_internet_up{target="1.1.1.1"} 1
cloner_internet_any_up 1
cloner_gateway_up{gateway="10.40.0.1"} 1
cloner_ping_rtt_ms{target="8.8.8.8"} 4.321
cloner_ping_rtt_ms{target="1.1.1.1"} 3.876
cloner_ping_rtt_ms{target="gateway(10.40.0.1)"} 0.412

Alertas Prometheus

Alerta Condição Severidade
ClonerInternetDown Ambos 8.8.8.8 e 1.1.1.1 falham por 2 min critical
ClonerInternetPartialLoss Apenas 1 dos 2 OK por 5 min warning
ClonerJobStuck Job em running > 30 min warning
ClonerNoAgent Métrica ausente por 5 min warning

Troubleshooting

Pod não inicia

kubectl describe pod -n web-agents -l app.kubernetes.io/name=cloner
kubectl logs -n web-agents -l app.kubernetes.io/name=cloner --previous 2>/dev/null
Sintoma Causa Solução
ImagePullBackOff Imagem não publicada Build local ou aguardar CI push
PVC cloned-sites Pending (web-agents ou clone-persona-N) NFS server não pronto OU volumeName inválido kubectl -n web-agents get pod -l app.kubernetes.io/name=nfs-server. Se Ready: kubectl describe pv cloned-sites-writer. PVs estão em k8s/dut/36-cloned-sites-pvs.yaml.
Slot MountVolume.SetUp failed com mount.nfs: Connection refused NFS server caiu kubectl -n web-agents rollout restart deploy/nfs-server. Conteúdo persiste no hostPath do nó.
Slot pod sem dados após bind Cloner gravou em outro nó (multi-node bug pré-PR-H) Confirme cloner em role=infra e NFS server no MESMO nó. kubectl -n web-agents get pod -o wide.
CreateContainerError Secret cloner-secrets ausente ./scripts/secrets-init.sh
Init:Error routing-init falhou Ver seção abaixo
Cloner sem IP em net1 CNI DHCP daemon ausente do nó kubectl get ds cni-dhcp-daemon -n web-agents deve ter DESIRED=READY=número de nós.

VLAN 40 (net1) sem IP

# Dentro do pod
kubectl exec -n web-agents deploy/cloner -- ip addr show net1

# NAD correto?
kubectl get nad cloner-isp -n web-agents -o jsonpath='{.spec.config}' | python3 -m json.tool
# Deve mostrar: "master": "eth1.40"

# eth1.40 existe no node?
kubectl debug node/<nome-do-node> -it --image=alpine -- ip link show eth1.40

# Criar manualmente se ausente:
sudo ip link add link eth1 name eth1.40 type vlan id 40
sudo ip link set eth1.40 up
kubectl rollout restart deployment/cloner -n web-agents

Nexus 9000: verificar VLAN 40 no trunk de UCS-4:

show interfaces trunk | grep Eth1/<N>
! Se VLAN 40 não aparecer:
conf t
  int Eth1/<N>
    switchport trunk allowed vlan add 40

routing-init falhou

kubectl logs \
  $(kubectl get pod -n web-agents -l app.kubernetes.io/name=cloner -o name) \
  -c routing-init

Sucesso esperado:

[routing-init] ISP iface=net1 ip=10.40.0.X gw=10.40.0.1
[routing-init] gateway=10.40.0.1 written to /var/isp-config/gateway.txt
[routing-init] routing configured — all public TCP/UDP/ICMP exits via net1

WARNING: net1 has no IP after 60 s → DHCP da VLAN 40 não respondeu (ver seção acima).

Sem acesso à internet (ping vermelho)

kubectl exec -n web-agents deploy/cloner -- ip route show table 100
# Deve: default via 10.40.0.1 dev net1

kubectl exec -n web-agents deploy/cloner -- ip rule show
# Deve: 10: from all fwmark 0x64 lookup 100

kubectl exec -n web-agents deploy/cloner -- ping -c 3 -I net1 8.8.8.8

DNS não resolve

kubectl exec -n web-agents deploy/cloner -- cat /etc/resolv.conf
# Deve: nameserver 8.8.8.8 / 208.67.222.222 / 10.96.0.10

kubectl exec -n web-agents deploy/cloner -- nslookup example.com 8.8.8.8
kubectl exec -n web-agents deploy/cloner -- \
  nslookup dashboard.web-agents.svc.cluster.local 10.96.0.10

Erro de certificado HTTPS

kubectl exec -n web-agents deploy/cloner -- \
  curl -v https://example.com 2>&1 | grep -E "SSL|cert|error"
  • CA pública → deve funcionar sem configuração adicional (bundle Debian inclui Mozilla roots)
  • CA privada → injetar via ConfigMap (ver seção "Certificados privados")
  • SSL_ERROR_RX_RECORD_TOO_LONG → tráfego interceptado incorretamente; verificar que o cloner não passa pelo NGFW

Job travado em running

kubectl logs -n web-agents deploy/cloner -f

# Cancelar via API
curl -s -X DELETE https://agents.mylab.local/api/clone/jobs/<job-id> \
  -H "Authorization: Basic $(echo -n 'admin:<PASS>' | base64)"

# Ou diretamente no banco
kubectl exec -n web-agents deploy/dashboard -- psql "$DATABASE_URL" -c \
  "UPDATE clone_jobs SET status='cancelled', completed_at=now()
   WHERE id='<job-id>' AND status='running';"

Causas comuns: | Causa | Sinal | |-------|-------| | Anti-bot bloqueou | Log: net::ERR_BLOCKED ou timeout 60 s | | Volume NFS cheio | kubectl exec deploy/cloner -- df -h /mnt/cloned (capacidade=hostPath do nó do NFS server) | | NFS server fora do ar | Slot pods retornam 503/timeout; kubectl -n web-agents get pod -l app.kubernetes.io/name=nfs-server | | VLAN 40 perdeu rota | Gateway ping vermelho no Grafana |

Gateway ISP vermelho

kubectl exec -n web-agents deploy/cloner -- cat /var/isp-config/gateway.txt
GW=$(kubectl exec -n web-agents deploy/cloner -- cat /var/isp-config/gateway.txt)
kubectl exec -n web-agents deploy/cloner -- ping -c 3 $GW
kubectl exec -n web-agents deploy/cloner -- ip neigh show dev net1

Multi-node (4 UCS servers)

O cloner é fixado no UCS-4 (role=infra) pelo overlay patch-cloner-nodesel.yaml.

# Verificar node
kubectl get pod -n web-agents -l app.kubernetes.io/name=cloner -o wide
# NODE deve ser UCS-4

# VLAN 40 em UCS-4
kubectl debug node/<ucs4-hostname> -it --image=alpine -- ip link show eth1.40

Notas

  • Uma réplica — jobs executam sequencialmente. Para paralelismo, aumentar replicas.
  • Sobrescrita — clonar a mesma persona substitui o clone anterior.
  • Storage NFS RWX — em multi-node, o NFS server roda no UCS-4 (mesmo nó que o cloner). O hostPath subjacente fica em /var/lib/agent-cluster/cloned-sites no UCS-4. Slots em UCS-1 leem via NFS sobre OOBI (eth0). A capacidade depende do disco do UCS-4. Monitorar: kubectl -n web-agents exec deploy/cloner -- df -h /mnt/cloned.
  • OOBI vs data plane — todo I/O de storage atravessa OOBI. As VLANs 200-209 (slots) e VLAN 40 (cloner) carregam APENAS tráfego de teste agente↔NGFW↔persona, nunca NFS.
  • SPAs — sites que exigem interação do usuário para renderizar conteúdo podem ter clone incompleto.
  • robots.txt — ignorado (uso interno de lab).