Infraestrutura

O WordPress de cursos roda no cluster Kubernetes da Oracle Cloud (OKE), no namespace legacy-sites, dividindo os nós com a produção do FUJI. Foi migrado em setembro/2026 do docker-compose da VM Azure. Desde 02/10/2026 é gerenciado por GitOps (pasta legacy-sites/ do repositório gitops, aplicada pelo ArgoCD).

Visão geral

Item Valor

Cluster

OKE enj-cluster, região sa-saopaulo-1 (contexto kubectl: oracle)

Nós

2 × VM.Standard.E5.Flex (node pool fuji-main, on-demand), ~3,8 vCPU e ~13 GiB alocáveis por nó, compartilhados com a produção do FUJI

Namespace

legacy-sites (criado em 13/09/2026)

Entrada

ingress-nginx do cluster, load balancer 163.176.228.60 (o mesmo dos hosts *.fuji.net.br)

Certificado de origem

cert-manager, ClusterIssuer letsencrypt, desafio HTTP-01

DNS / CDN

Cloudflare, zona artesmarciaisonline.com.br, gerenciada manualmente no painel

Gestão

GitOps: pasta legacy-sites/ do repositório gitops, Application legacy-sites do ArgoCD (sync automático com prune, sem selfHeal). Ver GitOps

Recursos Kubernetes

Recurso Nome Detalhes

Deployment

wp-cursos

1 réplica, estratégia Recreate. Dois containers no mesmo pod:

* wordpress: imagem wordpress:4.9.8-php7.2-fpm (o core instalado no volume é o 4.9.9, PHP 7.2.12). Requests 100m / 384Mi, limits 1200m / 1,5Gi. * nginx: imagem nginx:1.25, escuta na 8080 e repassa o PHP para 127.0.0.1:9000. Requests 20m / 64Mi, limits 200m / 256Mi.

Service

wp-cursos

ClusterIP, porta 80 → 8080 (nginx do pod)

StatefulSet

mariadb

1 réplica, imagem mariadb:10.3.8. Requests 100m / 512Mi, limits 1 CPU / 2Gi.

Service

mariadb

ClusterIP, porta 3306 (mariadb.legacy-sites.svc.cluster.local)

PersistentVolumeClaim

wp-content-cursos

50Gi, oci-bv (Block Volume, RWO). Contém todo o webroot: core, wp-config.php, wp-content (temas, plugins, uploads, cache).

PersistentVolumeClaim

mysql-data

50Gi, oci-bv. Datadir do MariaDB. O banco enj ocupa ~400 MB depois da limpeza de 02/10/2026 (eram ~2,9 GB com o spam).

Ingress

legacy-sites

Classe nginx, proxy-body-size: 512m. Hosts cursos.artesmarciaisonline.com.br, artesmarciaisonline.com.br e sistemafuji.com.br (este sem TLS e sem tráfego).

Secret (TLS)

legacy-sites-tls

Let’s Encrypt, cobre cursos.artesmarciaisonline.com.br e artesmarciaisonline.com.br, renovado automaticamente pelo cert-manager.

ConfigMap

nginx-conf-cursos

Server block do nginx do pod. Inclui o bloqueio do XML-RPC (location ^~ /xmlrpc.php { return 403; }, que pega também /xmlrpc.php/…​) e try_files $uri =404; no bloco do PHP, para que o PHP-FPM só execute arquivos .php que existem. Ver Bloqueio do XML-RPC em duas camadas.

ConfigMap

wp-cursos-fpm-tuning

Montado em /usr/local/etc/php-fpm.d/zz-pool-tuning.conf: pm.max_children = 10, pm.start_servers = 3, pm.min_spare_servers = 2, pm.max_spare_servers = 4, pm.max_requests = 500.

Secret

mariadb-credentials

Senha do root do MariaDB (MYSQL_ROOT_PASSWORD).

Secret

wp-cursos-db

WORDPRESS_DB_HOST, WORDPRESS_DB_NAME (enj), WORDPRESS_DB_USER, WORDPRESS_DB_PASSWORD. Ver como a imagem usa esses valores.

Os Secrets são criados manualmente (o SOPS está desativado no projeto). As senhas do MariaDB foram trocadas em 02/10/2026 e só existem nos Secrets; não há cópia em repositório.

Outros recursos do namespace, fora do escopo deste módulo: static-root (página estática do apex) e wp-fuji (desligado, 0 réplicas, com seu próprio PVC wp-content-fuji).

DNS e Cloudflare

A zona artesmarciaisonline.com.br fica numa conta Cloudflare e é editada à mão no painel. Ela não está no Terraform do iac; trazê-la para lá é uma pendência registrada.

Registro Tipo Conteúdo Proxy

@

A

163.176.228.60 (OKE)

ligado

cursos

A

163.176.228.60 (OKE)

ligado

www

CNAME

artesmarciaisonline.com.br

ligado

ftp

A

20.195.168.150 (VM Azure antiga)

desligado — remover

Com o proxy ligado, existem dois certificados: o da borda (emitido pela Cloudflare e apresentado ao visitante) e o de origem (Let’s Encrypt, emitido pelo cert-manager no cluster). Desligar o proxy faz o visitante ver diretamente o certificado de origem; o cert-manager precisa de acesso HTTP direto ao /.well-known/acme-challenge/ para renovar.

Regras e recomendações do painel da zona:

  • regra de WAF (custom rule, ação Block) com a expressão starts_with(http.request.uri.path, "/xmlrpc.php"), ativa desde 02/10/2026 (ver Bloqueio do XML-RPC em duas camadas);

  • não ligar o Bot Fight Mode sem antes confirmar que nenhum retorno automático de pagamento (WooCommerce / Paid Memberships Pro) ou o ManageWP depende de acesso automatizado.

Bloqueio do XML-RPC em duas camadas

O XML-RPC foi o canal do spam e da força bruta no incidente (ver Incidente: invasão do WordPress de cursos (ago–out/2026)). Ele é bloqueado na borda e na origem:

Camada Regra Efeito

Cloudflare (WAF)

starts_with(http.request.uri.path, "/xmlrpc.php") → Block

A requisição nem chega ao cluster; o visitante vê a página de bloqueio da Cloudflare (403).

nginx do pod

location ^~ /xmlrpc.php { return 403; } + try_files $uri =404; no bloco \.php$

Segunda camada, caso o proxy seja desligado ou a regra do WAF removida.

A primeira versão do bloqueio (location = /xmlrpc.php no nginx e uri.path eq "/xmlrpc.php" no WAF) só casava o caminho exato. Com o cgi.fix_pathinfo padrão do PHP-FPM, POST /xmlrpc.php/xmlrpc.php executava o xmlrpc.php normalmente, e a força bruta seguiu por esse caminho até a correção, em 02/10/2026. O teste correto é funcional: um POST com o método inofensivo demo.sayHello não pode responder Hello! em nenhuma variação de caminho. A checagem periódica (/wp-invasao, no repositório iac) faz esse teste.

/XMLRPC.php (maiúsculas) passa pelo WAF, que diferencia maiúsculas, mas recebe 404 na origem, porque o arquivo não existe com esse nome.

GitOps

Tudo o que é Kubernetes está versionado em gitops/legacy-sites/ (Kustomize): Deployments, StatefulSet, Services, PVCs, Ingress e as configurações do nginx e do PHP-FPM, guardadas como arquivos .conf em config/. Uma mudança é um commit na main; o ArgoCD sincroniza sozinho. Antes do commit, conferir:

kustomize build legacy-sites | kubectl --context oracle -n legacy-sites diff -f -
Ponto Detalhe

Fora do Git

Os Secrets (mariadb-credentials, wp-cursos-db, wp-fuji-db), criados à mão, e o certificado, gerado pelo cert-manager. Core, plugins, temas, uploads e banco vivem nos PVCs: atualizar o WordPress não passa pelo Git.

PVCs

Anotados com Prune=false,Delete=false: o ArgoCD nunca apaga um volume.

Sem selfHeal

Uma mudança feita com kubectl fica valendo até o próximo sync (o app aparece como OutOfSync). É o que permite tirar o site do ar na hora numa suspeita de invasão. Para manter desligado, registrar replicas: 0 no Git em seguida, senão o próximo commit religa o site.

ConfigMaps

Mudar só a configuração não reinicia o pod. O nginx precisa de reload (nginx -t && nginx -s reload no container nginx); o tuning do PHP-FPM, de rollout restart. Mudar o template de um Deployment reinicia o pod (estratégia Recreate, alguns segundos fora do ar).

Sandbox (homologação)

Cópia do site para ensaiar atualizações sem tocar na produção, em legacy-sites-staging (gitops/legacy-sites-staging/, Application legacy-sites-staging). É um overlay de legacy-sites: mesmas imagens e configurações, só com wp-cursos e mariadb; o que difere está declarado no overlay.

Item Detalhe

Acesso

IP público próprio, 163.176.171.200 (NLB da OCI, gratuito, preserva o IP de quem acessa). Mesmo domínio da produção: quem testa mapeia cursos.artesmarciaisonline.com.br para esse IP, só no navegador (especialista/sandbox-chrome.sh, perfil separado) ou no /etc/hosts (especialista/wp-sandbox-hosts.sh). Não passa pela Cloudflare.

Proteção

TLS no nginx do pod com uma cópia do certificado da produção; senha (basic auth, Secret sandbox-htpasswd); noindex; os mesmos bloqueios do nginx da produção.

Isolamento

Mu-plugin sandbox-guard (ConfigMap montado no PVC): wp_mail() não envia nada (só registra [SANDBOX] e-mail bloqueado… no log), DISABLE_WP_CRON, WP_HTTP_BLOCK_EXTERNAL com liberação só para atualizações, faixa SANDBOX. O refresh.sh põe PayPal e PMPro em teste, desativa o ManageWP e troca os salts. Banco com senhas próprias, usuário só com acesso ao enj. Limite: plugin que fala com serviço externo por cURL direto não é bloqueado (o cluster não tem NetworkPolicy).

Cuidado ao clonar: o wp-config.php copiado da produção aponta para o banco de produção (DB_HOST com o nome completo mariadb.legacy-sites.svc.cluster.local). O refresh.sh reescreve os DB_* logo após a cópia e trava os ajustes se o banco não for o do sandbox. Em 02/10/2026, antes dessa trava, os ajustes chegaram a gravar na produção (ManageWP desativado e PayPal em modo de teste por ~7 minutos, sem pedidos no período); os valores originais foram restaurados.

Dados

Cópia fiel da produção (dados pessoais reais; acesso sob confidencialidade), feita pelo refresh.sh pod a pod dentro do cluster. Rodar de novo apaga o sandbox.

Versões em teste

Bloco images: do kustomization.yaml do overlay.

Custo

NLB sem custo; ~0,2 vCPU e ~0,9 GiB de requests nos nós compartilhados com o FUJI. Desligar com replicas: 0 quando não estiver em uso.

Operação do dia a dia

Comandos úteis
# estado dos pods
kubectl --context oracle -n legacy-sites get pods

# logs de acesso (nginx) e do PHP-FPM
kubectl --context oracle -n legacy-sites logs deploy/wp-cursos -c nginx --since=10m
kubectl --context oracle -n legacy-sites logs deploy/wp-cursos -c wordpress --since=10m

# console SQL (a senha vem do ambiente do próprio pod; aspas simples são obrigatórias)
kubectl --context oracle -n legacy-sites exec -it mariadb-0 -- \
  sh -c 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" enj'

# reiniciar o WordPress (estratégia Recreate: há alguns segundos de indisponibilidade)
kubectl --context oracle -n legacy-sites rollout restart deployment/wp-cursos

# tirar o site do ar rapidamente (ex.: nova suspeita de invasão); registrar replicas: 0 no gitops depois
kubectl --context oracle -n legacy-sites scale deployment/wp-cursos --replicas=0
Não existe phpMyAdmin no cluster, de propósito. Para acesso gráfico ao banco, use kubectl port-forward svc/mariadb 3306 e um cliente local, só durante o uso.

Lacunas conhecidas

Lacuna Situação

Backup

Backup lógico diário (para um bucket OCI) e snapshot dos discos implementados em 03/10/2026, ver Backup e restore. A lacuna só fecha depois da ativação e do primeiro backup conferido. O plugin UpdraftPlus está instalado, mas a configuração não foi verificada. Antes disso, o último backup conhecido é o dump de 02/10/2026 (anterior à limpeza), guardado no disco da VM Azure (/opt/docker/backups/migracao-k8s/), hoje desalocada.

Isolamento de rede

O cluster usa Flannel sem controller de NetworkPolicy: um pod comprometido do WordPress alcança o Postgres de produção do FUJI e o serviço de metadados da instância (IMDS) da Oracle.

Isolamento de nós

Roda nos mesmos nós da produção do FUJI. Um pico de CPU aqui afeta o FUJI.

Monitoramento

A verificação externa (check.sh + healthchecks.io) rodava pela crontab da VM Azure e parou com a desalocação da VM. Hoje não há monitoramento externo do site.

Desempenho de disco

Os PVCs usam o tier padrão do oci-bv, sem vpusPerGB definido.

Software

WordPress 4.9.9, PHP 7.2, MariaDB 10.3 e tema pirateado, todos sem suporte. Ver Guia de atualização.

Ambiente anterior (VM Azure)

Até 02/10/2026 o site rodava num docker-compose (/opt/docker) na VM Azure fujivm.brazilsouth.cloudapp.azure.com (20.195.168.150), junto com o PostgreSQL do FUJI. Os containers foram parados em 02/10/2026 com restart=no, e os volumes foram preservados como evidência do incidente. Ainda em 02/10/2026 a VM aparecia como desalocada (VM deallocated). O disco continua existindo e guarda os logs do nginx, o dump de 02/10 e os volumes originais; eles devem ser copiados antes de a VM ser destruída junto com o restante da Azure (item 6.6 da migração).