Código da Aula: Github

Crie a branch aula-15-security a partir da 02-ingress e entre nela

🐧 Linux / Mac e ⊞ Windows (mesmo comando):

git checkout 02-ingress
git checkout -b aula-15-security

⚙️ Parte 0 — Verificação do Ambiente

📁 Verificar Docker

🐧 Linux / Mac:

sudo systemctl start docker
docker info

⊞ Windows (PowerShell):

docker-machine start default
docker-machine regenerate-certs default -f
docker-machine env default | Invoke-Expression
docker info

No Windows com Docker Toolbox, o Docker roda dentro de uma VM VirtualBox. Esses 4 comandos garantem que a VM está rodando, com certificados válidos e o terminal apontando para ela.

📁 Verificar Terraform

🐧 Linux / Mac e ⊞ Windows (mesmo comando):

terraform version

Se precisar atualizar ou instalar: https://developer.hashicorp.com/terraform/install

Para adicionar o Terraform ao PATH no Windows (PowerShell):

[System.Environment]::SetEnvironmentVariable("PATH", $env:PATH + ";C:\terraform", "User")

📁 Verificar e instalar doctl

O doctl é a CLI oficial da DigitalOcean. Usamos ele para autenticar e salvar o kubeconfig do cluster automaticamente durante o terraform apply.

🐧 macOS:

brew install doctl
doctl version

🐧 Linux:

wget https://github.com/digitalocean/doctl/releases/download/v1.159.0/doctl-1.159.0-linux-amd64.tar.gz
tar xf doctl-1.159.0-linux-amd64.tar.gz
sudo mv doctl /usr/local/bin
doctl version

⊞ Windows (PowerShell):

Invoke-WebRequest https://github.com/digitalocean/doctl/releases/download/v1.159.0/doctl-1.159.0-windows-amd64.zip -OutFile doctl.zip
Expand-Archive -Path doctl.zip -DestinationPath C:\doctl
[System.Environment]::SetEnvironmentVariable("PATH", $env:PATH + ";C:\doctl", "User")
doctl version

📁 Autenticar o doctl com a DigitalOcean

  1. Acesse o Painel DigitalOcean → API → Tokens
  2. Clique em Generate New Token
  3. Dê um nome (ex: aula-15-k8s-security), marque read e write
  4. Copie o token gerado — ele só aparece uma vez

🐧 Linux / Mac e ⊞ Windows (mesmo comando):

doctl auth init --access-token SEU_TOKEN_AQUI
doctl account get

📁 Verificar versões disponíveis do Kubernetes

🐧 Linux / Mac e ⊞ Windows (mesmo comando):

doctl kubernetes options versions

Copie o Slug da versão mais recente (ex: 1.36.0-do.0) — você vai usar esse valor no terraform.tfvars.

🐳 Parte 1 — Imagens Docker

Nesta aula vamos utilizar as imagens que já estão publicadas no Docker Hub a partir das aulas anteriores:

📁 Verificar e baixar as imagens do Docker Hub

🐧 Linux / Mac e ⊞ Windows (mesmo comando):

docker pull toticavalcanti/fiber-auth-api:v1.0
docker pull toticavalcanti/auth-ui:v1.1

🐧 Linux / Mac:

docker images | grep toticavalcanti

⊞ Windows (PowerShell):

docker images | Select-String "toticavalcanti"

⊞ Windows (CMD):

docker images | findstr toticavalcanti

📁 Recap — Dockerfiles das aulas anteriores

📁 Ativar BuildKit

🐧 Linux / Mac:

export DOCKER_BUILDKIT=1

⊞ Windows (PowerShell):

$env:DOCKER_BUILDKIT=1

📁 Dockerfile do Backend

codigo-fluente-fiber-tutorial/devops/Dockerfile-backend

# Etapa 1: Build
FROM golang:1.18 as builder
WORKDIR /app
COPY backend/fiber-project/go.mod backend/fiber-project/go.sum ./
RUN go mod download
COPY backend/fiber-project/ ./
RUN go build -o main .

Etapa 2: Imagem final leve (distroless)

FROM gcr.io/distroless/base-debian10 WORKDIR /app COPY --from=builder /app/main . CMD ["./main"]

📁 Dockerfile do Frontend

codigo-fluente-fiber-tutorial/devops/Dockerfile-frontend

# Etapa 1: Build da aplicação React
FROM node:16-alpine as build
WORKDIR /app
RUN apk add --no-cache python3 make g++
COPY frontend/react-auth/package*.json ./
RUN npm ci --legacy-peer-deps --verbose
COPY frontend/react-auth/ ./
ENV REACT_APP_API_URL=/api
RUN npm run build

Etapa 2: Servir com Nginx

FROM nginx:alpine COPY --from=build /app/build /usr/share/nginx/html COPY frontend/react-auth/nginx.conf /etc/nginx/conf.d/default.conf COPY frontend/react-auth/docker-entrypoint.sh /docker-entrypoint.sh RUN chmod +x /docker-entrypoint.sh EXPOSE 80 ENTRYPOINT ["/docker-entrypoint.sh"] CMD ["nginx", "-g", "daemon off;"]

📁 Build e Push das imagens

🐧 Linux / Mac e ⊞ Windows (mesmo comando):

docker build -f devops/Dockerfile-backend -t toticavalcanti/fiber-auth-api:v1.0 .
docker build -f devops/Dockerfile-frontend -t toticavalcanti/auth-ui:v1.1 .

docker login docker push toticavalcanti/fiber-auth-api:v1.0 docker push toticavalcanti/auth-ui:v1.1

☁️ Parte 2 — Estrutura do Projeto Terraform

O Terraform faz tudo de uma vez: cria o cluster na DigitalOcean e deploya a aplicação com todas as configurações de segurança. Um único terraform apply — o mesmo padrão das aulas anteriores.

Nesta aula a estrutura fica assim:

user-auth-system/
└── devops/
    ├── main.tf                  ← MODIFICADO: versão, nome e região do cluster viram variáveis
    ├── variables.tf             ← MODIFICADO: adiciona cluster_name, cluster_version e demais variáveis
    ├── locals.tf                ← NOVO: padrões de segurança centralizados
    ├── rbac.tf                  ← NOVO: ServiceAccounts e Roles com menor privilégio
    ├── deployment.tf            ← MODIFICADO: SecurityContext no backend e frontend; MySQL sem SecurityContext
    ├── network_policy.tf        ← NOVO: Zero Trust networking entre pods
    ├── helm_ingress.tf          ← MODIFICADO: namespace dedicado + desabilita admission webhook
    ├── ingress.tf               ← sem mudanças
    ├── outputs.tf               ← sem mudanças
    ├── terraform.tfvars.example ← MODIFICADO: reflete todas as variáveis
    └── .gitignore               ← sem mudanças

📁 Arquivos modificados e criados nesta aula

devops/main.tf ← MODIFICADO

O que mudou em relação à aula 14 e por quê:

  • required_version — adicionamos >= 1.0 para garantir compatibilidade mínima do Terraform.
  • name = var.cluster_name — o nome do cluster deixa de ser hardcoded e passa a vir do terraform.tfvars.
  • region = var.do_region — a região deixa de ser hardcoded e passa a vir do terraform.tfvars.
  • version = var.cluster_version — a versão do Kubernetes deixa de ser hardcoded. Use o slug obtido via doctl kubernetes options versions.
  • sleep 120 — aumentado de 45 para 120 segundos para garantir que o DNS do cluster esteja propagado antes de criar os recursos Kubernetes.

user-auth-system/devops/main.tf

terraform {
  required_version = ">= 1.0"
  required_providers {
    digitalocean = {
      source  = "digitalocean/digitalocean"
      version = "~> 2.0"
    }
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = "2.33.0"
    }
    helm = {
      source  = "hashicorp/helm"
      version = "2.17.0"
    }
  }
}

provider "digitalocean" { token = var.do_token }

Criação do Cluster Kubernetes

resource "digitalocean_kubernetes_cluster" "meu_cluster" { name = var.cluster_name region = var.do_region version = var.cluster_version

node_pool { name = "default-pool" size = "s-2vcpu-4gb" node_count = 2 }

provisioner "local-exec" { command = <<-EOT echo "Iniciando kubeconfig..." doctl kubernetes cluster kubeconfig save ${var.cluster_name} echo "Kubeconfig salvo. Aguardando cluster ficar pronto..." sleep 120 kubectl wait --for=condition=Ready nodes --all --timeout=300s echo "Cluster está pronto!" EOT } }

provider "kubernetes" { host = digitalocean_kubernetes_cluster.meu_cluster.endpoint token = digitalocean_kubernetes_cluster.meu_cluster.kube_config[0].token cluster_ca_certificate = base64decode( digitalocean_kubernetes_cluster.meu_cluster.kube_config[0].cluster_ca_certificate ) }

provider "helm" { kubernetes { host = digitalocean_kubernetes_cluster.meu_cluster.endpoint token = digitalocean_kubernetes_cluster.meu_cluster.kube_config[0].token cluster_ca_certificate = base64decode( digitalocean_kubernetes_cluster.meu_cluster.kube_config[0].cluster_ca_certificate ) } }

devops/variables.tf ← MODIFICADO

O que mudou e por quê:

  • do_region, cluster_name, cluster_version — região e versão do cluster deixam de ser hardcoded.
  • namespace, environment — permitem identificar o ambiente nos labels dos recursos.
  • backend_image, frontend_image, mysql_image — imagens Docker via variáveis em vez de hardcoded.
  • backend_replica_count — número de réplicas do backend via variável.
  • mysql_database — nome do banco via variável.

user-auth-system/devops/variables.tf

variable "do_token" {
  type        = string
  sensitive   = true
  description = "Token de acesso para a API da DigitalOcean"
}

variable "do_region" { description = "Região onde o cluster será criado" type = string default = "nyc1" }

variable "cluster_name" { description = "Nome do cluster Kubernetes" type = string default = "meu-cluster" }

variable "cluster_version" { description = "Versão do Kubernetes (use: doctl kubernetes options versions)" type = string default = "1.36.0-do.0" }

variable "namespace" { description = "Namespace Kubernetes para os recursos" type = string default = "default" }

variable "environment" { description = "Ambiente de execução" type = string default = "production" }

variable "backend_image" { description = "Imagem Docker do backend Go/Fiber" type = string default = "toticavalcanti/fiber-auth-api:v1.0" }

variable "frontend_image" { description = "Imagem Docker do frontend React/Nginx" type = string default = "toticavalcanti/auth-ui:v1.1" }

variable "mysql_image" { description = "Imagem Docker do MySQL" type = string default = "mysql:8.0" }

variable "backend_replica_count" { description = "Número de réplicas do backend" type = number default = 1 }

variable "mysql_database" { description = "Nome do banco de dados MySQL" type = string default = "auth_db" }

variable "mysql_root_password" { description = "Senha do root para o MySQL" type = string sensitive = true }

variable "gmail_username" { type = string description = "Endereço do Gmail para enviar emails" }

variable "gmail_password" { type = string sensitive = true description = "Senha do Gmail ou Senha de App" }

variable "react_app_api_url" { description = "URL para a API do auth-api" type = string default = "/api/" }

devops/locals.tf ← NOVO

Centraliza os padrões de segurança reutilizados em todos os recursos. Em vez de repetir as mesmas configurações de SecurityContext em cada container, definimos uma vez aqui e referenciamos em todo lugar.

  • common_labels — labels aplicados em todos os recursos para rastreabilidade.
  • pod_security_context e container_security_context — definem que nenhum container roda como root, o filesystem é somente leitura e todas as Linux capabilities são removidas. Aplicados no backend e frontend. O MySQL não recebe SecurityContext pois a imagem oficial requer privilégios de root para inicializar — o isolamento do MySQL é garantido pela NetworkPolicy.
  • resource limits — CPU e memória definidos para backend, frontend e MySQL.

user-auth-system/devops/locals.tf

locals {
  common_labels = {
    project    = "fiber-auth"
    managed-by = "terraform"
  }

pod_security_context = { run_as_non_root = true run_as_user = 65534 run_as_group = 65534 fs_group = 65534 }

container_security_context = { allow_privilege_escalation = false read_only_root_filesystem = true run_as_non_root = true run_as_user = 65534 drop_capabilities = ["ALL"] }

backend_resources = { requests = { cpu = "100m", memory = "128Mi" } limits = { cpu = "500m", memory = "512Mi" } }

frontend_resources = { requests = { cpu = "50m", memory = "64Mi" } limits = { cpu = "200m", memory = "256Mi" } }

mysql_resources = { requests = { cpu = "250m", memory = "512Mi" } limits = { cpu = "500m", memory = "1Gi" } } }

devops/rbac.tf ← NOVO

Implementa o princípio do menor privilégio: cada serviço tem sua própria identidade no cluster e só pode fazer o que é estritamente necessário.

  • ServiceAccounts separadas — uma para backend, uma para frontend, uma para MySQL.
  • automount_service_account_token = false — impede que o token seja montado automaticamente nos pods.
  • Role readonly_debugger — o backend pode ver logs para debug, mas não pode deletar pods ou criar secrets.
  • Frontend e MySQL sem nenhuma Role — acesso zero à API do Kubernetes.

user-auth-system/devops/rbac.tf

resource "kubernetes_service_account" "backend" {
  metadata {
    name   = "backend-service-account"
    labels = local.common_labels
  }
  automount_service_account_token = false
}

resource "kubernetes_service_account" "frontend" { metadata { name = "frontend-service-account" labels = local.common_labels } automount_service_account_token = false }

resource "kubernetes_service_account" "mysql" { metadata { name = "mysql-service-account" labels = local.common_labels } automount_service_account_token = false }

resource "kubernetes_role" "readonly_debugger" { metadata { name = "readonly-debugger" labels = local.common_labels }

rule { api_groups = [""] resources = ["pods", "pods/log"] verbs = ["get", "list"] } }

resource "kubernetes_role_binding" "backend_readonly" { metadata { name = "backend-readonly-binding" labels = local.common_labels }

role_ref { api_group = "rbac.authorization.k8s.io" kind = "Role" name = kubernetes_role.readonly_debugger.metadata[0].name }

subject { kind = "ServiceAccount" name = kubernetes_service_account.backend.metadata[0].name } }

devops/deployment.tf ← MODIFICADO

O que mudou em relação à aula 14 e por quê:

  • service_account_name — cada deployment usa a ServiceAccount específica criada no rbac.tf.
  • automount_service_account_token = false — o token não é montado automaticamente.
  • SecurityContext no backend e frontendrun_as_non_root, read_only_root_filesystem, capabilities drop ALL e seccomp_profile RuntimeDefault.
  • MySQL sem SecurityContext — a imagem oficial do MySQL requer privilégios de root para inicializar o banco. O isolamento é garantido via NetworkPolicy.
  • Volumes tmpfs/tmp, /var/cache/nginx e /var/run montados em RAM porque o filesystem é somente leitura.
  • Resource limits via locals — CPU e memória via locals.tf.
  • Imagens, banco e réplicas via variáveisvar.mysql_image, var.backend_image, var.frontend_image, var.mysql_database, var.backend_replica_count.
  • namespace = "ingress-nginx" no data source — o Ingress Controller agora roda em namespace dedicado.

user-auth-system/devops/deployment.tf

# Data Source para o IP do Ingress Controller
data "kubernetes_service" "ingress_nginx" {
  metadata {
    name      = "ingress-nginx-controller"
    namespace = "ingress-nginx"
  }
  depends_on = [helm_release.ingress_nginx]
}

##################################

SECRETS

################################## resource "kubernetes_secret" "gmail_credentials" { metadata { name = "gmail-credentials" } data = { username = var.gmail_username password = var.gmail_password } }

resource "kubernetes_secret" "mysql_secret" { metadata { name = "mysql-secret" } data = { mysql-root-password = base64encode(var.mysql_root_password) } }

##################################

VOLUMES

################################## resource "kubernetes_persistent_volume" "mysql_pv" { metadata { name = "mysql-pv" labels = { type = "local" app = "mysql" } } spec { capacity = { storage = "5Gi" } access_modes = ["ReadWriteOnce"] storage_class_name = "manual" persistent_volume_reclaim_policy = "Retain"

persistent_volume_source {
  host_path {
    path = "/mnt/data"
    type = "DirectoryOrCreate"
  }
}

} }

resource "kubernetes_persistent_volume_claim" "mysql_pvc" { metadata { name = "mysql-pvc" labels = { app = "mysql" } } spec { access_modes = ["ReadWriteOnce"] resources { requests = { storage = "5Gi" } } storage_class_name = "manual" volume_name = kubernetes_persistent_volume.mysql_pv.metadata[0].name } depends_on = [ kubernetes_persistent_volume.mysql_pv ] }

##################################

MYSQL: DEPLOYMENT E SERVICE

################################## resource "kubernetes_deployment" "mysql" { metadata { name = "mysql" labels = { app = "mysql" } } spec { replicas = 1 selector { match_labels = { app = "mysql" } } template { metadata { labels = { app = "mysql" } } spec { service_account_name = kubernetes_service_account.mysql.metadata[0].name automount_service_account_token = false

    # A imagem oficial do MySQL requer root para inicializar o banco.
    # O isolamento é garantido via NetworkPolicy — apenas o backend acessa o MySQL.

    container {
      name  = "mysql"
      image = var.mysql_image

      env {
        name  = "MYSQL_ROOT_PASSWORD"
        value = var.mysql_root_password
      }

      env {
        name  = "MYSQL_DATABASE"
        value = var.mysql_database
      }

      resources {
        limits = {
          memory = local.mysql_resources.limits.memory
          cpu    = local.mysql_resources.limits.cpu
        }
        requests = {
          memory = local.mysql_resources.requests.memory
          cpu    = local.mysql_resources.requests.cpu
        }
      }

      port {
        container_port = 3306
        name           = "mysql"
      }

      volume_mount {
        name       = "mysql-persistent-storage"
        mount_path = "/var/lib/mysql"
      }

      readiness_probe {
        tcp_socket {
          port = 3306
        }
        initial_delay_seconds = 15
        period_seconds        = 10
      }

      liveness_probe {
        tcp_socket {
          port = 3306
        }
        initial_delay_seconds = 20
        period_seconds        = 10
      }
    }

    volume {
      name = "mysql-persistent-storage"
      persistent_volume_claim {
        claim_name = kubernetes_persistent_volume_claim.mysql_pvc.metadata[0].name
      }
    }
  }
}

} depends_on = [kubernetes_persistent_volume_claim.mysql_pvc] }

resource "kubernetes_service" "mysql_service" { metadata { name = "mysql-service" labels = { app = "mysql" } } spec { selector = { app = "mysql" } type = "ClusterIP" port { port = 3306 target_port = 3306 name = "mysql" } } }

##################################

BACKEND: DEPLOYMENT E SERVICE

################################## resource "kubernetes_deployment" "auth_api" { metadata { name = "auth-api" labels = { app = "auth-api" } } spec { replicas = var.backend_replica_count selector { match_labels = { app = "auth-api" } } template { metadata { labels = { app = "auth-api" } } spec { service_account_name = kubernetes_service_account.backend.metadata[0].name automount_service_account_token = false

    security_context {
      run_as_non_root = local.pod_security_context.run_as_non_root
      run_as_user     = local.pod_security_context.run_as_user
      run_as_group    = local.pod_security_context.run_as_group
      fs_group        = local.pod_security_context.fs_group
      seccomp_profile { type = "RuntimeDefault" }
    }

    init_container {
      name  = "wait-for-mysql"
      image = "busybox:1.28"
      command = [
        "sh", "-c",
        "until nc -z mysql-service 3306; do echo waiting for mysql; sleep 2; done;"
      ]
      security_context {
        allow_privilege_escalation = false
        read_only_root_filesystem  = true
        run_as_non_root            = true
        run_as_user                = 65534
        capabilities { drop = ["ALL"] }
      }
    }

    container {
      name  = "auth-api"
      image = var.backend_image

      env {
        name  = "MYSQL_ROOT_PASSWORD"
        value = var.mysql_root_password
      }

      env {
        name  = "DB_DSN"
        value = "root:${var.mysql_root_password}@tcp(mysql-service:3306)/${var.mysql_database}?parseTime=true"
      }

      env {
        name = "GMAIL_USERNAME"
        value_from {
          secret_key_ref {
            name = kubernetes_secret.gmail_credentials.metadata[0].name
            key  = "username"
          }
        }
      }

      env {
        name = "GMAIL_PASSWORD"
        value_from {
          secret_key_ref {
            name = kubernetes_secret.gmail_credentials.metadata[0].name
            key  = "password"
          }
        }
      }

      env {
        name  = "APP_URL"
        value = "http://${data.kubernetes_service.ingress_nginx.status[0].load_balancer[0].ingress[0].ip}"
      }

      security_context {
        allow_privilege_escalation = local.container_security_context.allow_privilege_escalation
        read_only_root_filesystem  = local.container_security_context.read_only_root_filesystem
        run_as_non_root            = local.container_security_context.run_as_non_root
        run_as_user                = local.container_security_context.run_as_user
        capabilities { drop = local.container_security_context.drop_capabilities }
      }

      port {
        container_port = 3000
        name           = "http"
      }

      resources {
        limits = {
          cpu    = local.backend_resources.limits.cpu
          memory = local.backend_resources.limits.memory
        }
        requests = {
          cpu    = local.backend_resources.requests.cpu
          memory = local.backend_resources.requests.memory
        }
      }

      volume_mount {
        name       = "tmp-dir"
        mount_path = "/tmp"
      }

      readiness_probe {
        http_get {
          path = "/api/health"
          port = 3000
        }
        initial_delay_seconds = 15
        period_seconds        = 10
      }

      liveness_probe {
        http_get {
          path = "/api/health"
          port = 3000
        }
        initial_delay_seconds = 20
        period_seconds        = 10
      }
    }

    volume {
      name = "tmp-dir"
      empty_dir { medium = "Memory" }
    }
  }
}

} depends_on = [ kubernetes_deployment.mysql, kubernetes_service.mysql_service ] }

resource "kubernetes_service" "auth_api" { metadata { name = "auth-api-service" labels = { app = "auth-api" } } spec { selector = { app = "auth-api" } type = "ClusterIP" port { port = 3000 target_port = 3000 name = "http" } } }

##################################

FRONTEND: DEPLOYMENT E SERVICE

################################## resource "kubernetes_deployment" "auth_ui" { metadata { name = "auth-ui" labels = { app = "auth-ui" } } spec { replicas = 1 selector { match_labels = { app = "auth-ui" } } template { metadata { labels = { app = "auth-ui" } } spec { service_account_name = kubernetes_service_account.frontend.metadata[0].name automount_service_account_token = false

    security_context {
      run_as_non_root = true
      run_as_user     = 101
      run_as_group    = 101
      fs_group        = 101
      seccomp_profile { type = "RuntimeDefault" }
    }

    container {
      name  = "auth-ui"
      image = var.frontend_image

      env {
        name  = "REACT_APP_API_URL"
        value = var.react_app_api_url
      }

      command = ["/bin/sh", "-c"]
      args = [
        <<-EOT
          echo 'window._env_ = { REACT_APP_API_URL: \"${var.react_app_api_url}\" };' > /usr/share/nginx/html/config.js
          nginx -g 'daemon off;'
        EOT
      ]

      security_context {
        allow_privilege_escalation = false
        read_only_root_filesystem  = true
        run_as_non_root            = true
        run_as_user                = 101
        capabilities { drop = ["ALL"] }
      }

      port {
        container_port = 80
        name           = "http"
      }

      resources {
        limits = {
          cpu    = local.frontend_resources.limits.cpu
          memory = local.frontend_resources.limits.memory
        }
        requests = {
          cpu    = local.frontend_resources.requests.cpu
          memory = local.frontend_resources.requests.memory
        }
      }

      volume_mount {
        name       = "nginx-cache"
        mount_path = "/var/cache/nginx"
      }
      volume_mount {
        name       = "nginx-run"
        mount_path = "/var/run"
      }
      volume_mount {
        name       = "tmp-dir"
        mount_path = "/tmp"
      }

      readiness_probe {
        http_get {
          path = "/index.html"
          port = 80
        }
        initial_delay_seconds = 10
        period_seconds        = 5
        failure_threshold     = 3
        success_threshold     = 1
        timeout_seconds       = 1
      }

      liveness_probe {
        http_get {
          path = "/index.html"
          port = 80
        }
        initial_delay_seconds = 15
        period_seconds        = 10
        failure_threshold     = 3
        success_threshold     = 1
        timeout_seconds       = 1
      }
    }

    volume {
      name = "nginx-cache"
      empty_dir { medium = "Memory" }
    }
    volume {
      name = "nginx-run"
      empty_dir { medium = "Memory" }
    }
    volume {
      name = "tmp-dir"
      empty_dir { medium = "Memory" }
    }
  }
}

}

depends_on = [ kubernetes_deployment.auth_api ] }

resource "kubernetes_service" "auth_ui" { metadata { name = "auth-ui-service" labels = { app = "auth-ui" } } spec { selector = { app = "auth-ui" } type = "ClusterIP" port { port = 80 target_port = 80 name = "http" } } }

devops/network_policy.tf ← NOVO

Implementa Zero Trust networking — tudo bloqueado por padrão, só o que é explicitamente permitido funciona.

Nossa arquitetura Zero Trust após esta aula:

  • Frontend → Backend (porta 3000): ✅ permitido
  • Backend → MySQL (porta 3306): ✅ permitido
  • Backend → Gmail SMTP (porta 587): ✅ permitido
  • Frontend → MySQL diretamente: ❌ bloqueado
  • Qualquer pod invasor → qualquer serviço: ❌ bloqueado
  • default-deny-all — bloqueia TODO tráfego Ingress e Egress no namespace.
  • backend-ingress-policy — o backend só aceita do frontend (porta 3000) ou do Ingress Controller.
  • backend-egress-policy — o backend só pode sair para MySQL (3306), Gmail SMTP (587, 465) e DNS (53).
  • frontend-policy — o frontend só aceita do Ingress Controller e só fala com o backend. Nunca acessa o MySQL diretamente.
  • mysql-ingress-policy — o MySQL só aceita conexões do backend.

user-auth-system/devops/network_policy.tf

resource "kubernetes_network_policy" "default_deny_all" {
  metadata {
    name   = "default-deny-all"
    labels = local.common_labels
  }
  spec {
    pod_selector {}
    policy_types = ["Ingress", "Egress"]
  }
}

resource "kubernetes_network_policy" "backend_ingress" { metadata { name = "backend-ingress-policy" labels = local.common_labels } spec { pod_selector { match_labels = { app = "auth-api" } } policy_types = ["Ingress"]

ingress {
  from {
    pod_selector {
      match_labels = {
        app = "auth-ui"
      }
    }
  }
  ports {
    port     = "3000"
    protocol = "TCP"
  }
}

ingress {
  from {
    namespace_selector {
      match_labels = {
        "kubernetes.io/metadata.name" = "ingress-nginx"
      }
    }
  }
  ports {
    port     = "3000"
    protocol = "TCP"
  }
}

} }

resource "kubernetes_network_policy" "backend_egress" { metadata { name = "backend-egress-policy" labels = local.common_labels } spec { pod_selector { match_labels = { app = "auth-api" } } policy_types = ["Egress"]

egress {
  to {
    pod_selector {
      match_labels = {
        app = "mysql"
      }
    }
  }
  ports {
    port     = "3306"
    protocol = "TCP"
  }
}

egress {
  ports {
    port     = "587"
    protocol = "TCP"
  }
}

egress {
  ports {
    port     = "465"
    protocol = "TCP"
  }
}

egress {
  ports {
    port     = "53"
    protocol = "UDP"
  }
}

egress {
  ports {
    port     = "53"
    protocol = "TCP"
  }
}

} }

resource "kubernetes_network_policy" "frontend_policy" { metadata { name = "frontend-policy" labels = local.common_labels } spec { pod_selector { match_labels = { app = "auth-ui" } } policy_types = ["Ingress", "Egress"]

ingress {
  from {
    namespace_selector {
      match_labels = {
        "kubernetes.io/metadata.name" = "ingress-nginx"
      }
    }
  }
  ports {
    port     = "80"
    protocol = "TCP"
  }
}

egress {
  to {
    pod_selector {
      match_labels = {
        app = "auth-api"
      }
    }
  }
  ports {
    port     = "3000"
    protocol = "TCP"
  }
}

egress {
  ports {
    port     = "53"
    protocol = "UDP"
  }
}

} }

resource "kubernetes_network_policy" "mysql_ingress" { metadata { name = "mysql-ingress-policy" labels = local.common_labels } spec { pod_selector { match_labels = { app = "mysql" } } policy_types = ["Ingress", "Egress"]

ingress {
  from {
    pod_selector {
      match_labels = {
        app = "auth-api"
      }
    }
  }
  ports {
    port     = "3306"
    protocol = "TCP"
  }
}

} }

devops/helm_ingress.tf ← MODIFICADO

O que mudou e por quê:

  • namespace = "ingress-nginx" e create_namespace = true — o Ingress Controller agora roda em namespace dedicado, que é o padrão recomendado. Evita conflitos com recursos do namespace default.
  • admissionWebhooks.enabled = false — o job de admission webhook falha em clusters novos porque tenta validar recursos antes do controller estar pronto. Desabilitar resolve o problema sem impacto funcional.

user-auth-system/devops/helm_ingress.tf

resource "helm_release" "ingress_nginx" {
  name             = "ingress-nginx"
  repository       = "https://kubernetes.github.io/ingress-nginx"
  chart            = "ingress-nginx"
  namespace        = "ingress-nginx"
  create_namespace = true
  timeout          = 600

Precisamos do Service tipo LoadBalancer

para a DigitalOcean provisionar IP externo

set { name = "controller.service.type" value = "LoadBalancer" }

Publicar service do controller para relatarmos IP

set { name = "controller.publishService.enabled" value = "true" }

Desabilitar admission webhook que causa falha no job de pre-install

set { name = "controller.admissionWebhooks.enabled" value = "false" } }

devops/terraform.tfvars.example ← MODIFICADO

O que mudou e por quê:

  • Reflete todas as variáveis do variables.tf.
  • O cluster_version atualizado para 1.36.0-do.0 — sempre verifique a versão mais recente com doctl kubernetes options versions.

user-auth-system/devops/terraform.tfvars.example

# cp terraform.tfvars.example terraform.tfvars
# NUNCA commitar o terraform.tfvars

do_token = "SEU_TOKEN_DIGITALOCEAN" do_region = "nyc1" cluster_name = "meu-cluster" cluster_version = "1.36.0-do.0"

namespace = "default" environment = "production"

backend_image = "toticavalcanti/fiber-auth-api:v1.0" frontend_image = "toticavalcanti/auth-ui:v1.1" mysql_image = "mysql:8.0" backend_replica_count = 1

mysql_database = "auth_db" mysql_root_password = "SUBSTITUA_POR_SENHA_FORTE"

gmail_username = "SEU_EMAIL@gmail.com" gmail_password = "SENHA_DE_APP_GMAIL"

react_app_api_url = "/api/"

🧪 Parte 3 — Deploy Passo a Passo

Passo 1: Preparar o terraform.tfvars

🐧 Linux / Mac:

cd devops
cp terraform.tfvars.example terraform.tfvars
nano terraform.tfvars

⊞ Windows (PowerShell):

cd devops
Copy-Item terraform.tfvars.example terraform.tfvars
notepad terraform.tfvars

⊞ Windows (CMD):

cd devops
copy terraform.tfvars.example terraform.tfvars
notepad terraform.tfvars

Preencha com seus dados reais: do_token, cluster_version, mysql_root_password, gmail_username e gmail_password.

Passo 2: Inicializar o Terraform

🐧 Linux / Mac e ⊞ Windows (mesmo comando):

terraform init

Passo 3: Verificar o plano

🐧 Linux / Mac e ⊞ Windows (mesmo comando):

terraform plan

Passo 4: Criar o cluster e fazer o deploy

🐧 Linux / Mac e ⊞ Windows (mesmo comando):

terraform apply

Digite yes quando solicitado. O processo leva cerca de 5 a 10 minutos.

Passo 5: Atualizar o kubeconfig

Após o terraform apply, o kubeconfig gerado automaticamente pode apontar para uma versão antiga do doctl. Execute este comando para garantir que está usando o doctl correto:

🐧 Linux / Mac e ⊞ Windows (mesmo comando):

doctl kubernetes cluster kubeconfig save meu-cluster --set-current-context

Passo 6: Verificar o cluster e os pods

🐧 Linux / Mac e ⊞ Windows (mesmo comando):

kubectl get nodes
kubectl get pods
kubectl get svc
kubectl get svc -n ingress-nginx
kubectl get networkpolicies
kubectl get serviceaccounts,roles,rolebindings
kubectl get secrets

Passo 7: Aguardar todos os pods ficarem Ready

🐧 Linux / Mac e ⊞ Windows (mesmo comando):

kubectl wait --for=condition=ready pod -l app=auth-api --timeout=120s
kubectl wait --for=condition=ready pod -l app=auth-ui --timeout=120s
kubectl wait --for=condition=ready pod -l app=mysql --timeout=120s

🔒 Parte 4 — Testes de Segurança

✅ Teste 1: Fluxo legítimo funcionando

🐧 Linux / Mac:

curl http://SEU_IP_DO_INGRESS/api/health

⊞ Windows (PowerShell):

Invoke-WebRequest -Uri "http://SEU_IP_DO_INGRESS/api/health" | Select-Object -ExpandProperty Content

❌ Teste 2: Frontend não acessa MySQL (deve ser bloqueado)

O comando vai travar por timeout — isso confirma que a NetworkPolicy está bloqueando o acesso.

🐧 Linux / Mac e ⊞ Windows (mesmo comando):

kubectl exec deployment/auth-ui -- nc -zv -w 5 mysql-service 3306

❌ Teste 3: Pod invasor sem labels (deve ser bloqueado)

🐧 Linux / Mac e ⊞ Windows (mesmo comando):

kubectl run invasor --image=busybox:1.36 --restart=Never -- sh -c "nc -zv -w 5 auth-api-service 3000 || echo 'ACESSO NEGADO'"
kubectl logs invasor
kubectl delete pod invasor

🧪 Teste 4: RBAC em ação

🐧 Linux / Mac:

# Frontend NAO pode listar Secrets — esperado: no
kubectl auth can-i get secrets \
  --as=system:serviceaccount:default:frontend-service-account

Backend PODE listar pods — esperado: yes

kubectl auth can-i list pods
--as=system:serviceaccount:default:backend-service-account

Backend NAO pode deletar pods — esperado: no

kubectl auth can-i delete pods
--as=system:serviceaccount:default:backend-service-account

⊞ Windows (PowerShell):

kubectl auth can-i get secrets `
  --as=system:serviceaccount:default:frontend-service-account

kubectl auth can-i list pods ` --as=system:serviceaccount:default:backend-service-account

kubectl auth can-i delete pods ` --as=system:serviceaccount:default:backend-service-account

📊 Checklist de Segurança

  • ☑️ RBAC: ServiceAccounts separadas com permissões mínimas
  • ☑️ NetworkPolicy deny-all: tudo bloqueado por padrão
  • ☑️ NetworkPolicy frontend→backend: frontend só acessa backend na porta 3000
  • ☑️ NetworkPolicy backend→MySQL: só o backend acessa MySQL na porta 3306
  • ☑️ NetworkPolicy backend→Gmail: backend pode sair na porta 587
  • ☑️ SecurityContext: runAsNonRoot, readOnlyRootFilesystem, capabilities drop ALL (backend e frontend)
  • ☑️ Resource limits: CPU e memória em todos os containers
  • ☑️ Secrets: MySQL e Gmail via kubernetes_secret, nunca hardcoded
  • ☑️ .gitignore: terraform.tfvars e *.tfstate protegidos

🚨 Troubleshooting

❓ kubectl não conecta após o terraform apply

O kubeconfig pode estar apontando para uma versão antiga do doctl. Execute:

🐧 Linux / Mac e ⊞ Windows (mesmo comando):

doctl kubernetes cluster kubeconfig save meu-cluster --set-current-context

❓ Backend não conecta ao MySQL após NetworkPolicy

🐧 Linux / Mac:

kubectl get pods --show-labels | grep auth-api

⊞ Windows (PowerShell):

kubectl get pods --show-labels | Select-String "auth-api"

❓ NetworkPolicy não está bloqueando

🐧 Linux / Mac:

kubectl get pods -n kube-system | grep cilium

⊞ Windows (PowerShell):

kubectl get pods -n kube-system | Select-String "cilium"

O DOKS usa Cilium como CNI desde 2023 — NetworkPolicies são suportadas nativamente.

✅ Conclusão

Parabéns! Você blindou o projeto fiber-auth-api + auth-ui com 5 camadas de segurança:

  • 🔐 RBAC granular: identidades separadas, permissões mínimas
  • 🏰 Zero Trust de rede: frontend não acessa MySQL, invasores bloqueados
  • 🛡️ Containers endurecidos: não-root, filesystem readonly, capabilities removidas (backend e frontend)
  • 🗝️ Secrets gerenciados: MySQL e Gmail via kubernetes_secret, nunca hardcoded
  • Resource limits: DoS prevenido em todos os containers

🚀 Próximos Passos

  • Aula 16: Pod Security Standards (PSS/PSA)
  • Aula 17: OPA Gatekeeper
  • Aula 18: Falco — monitoramento em runtime
  • Aula 19: cert-manager — TLS automático com Let's Encrypt
  • Aula 20: Istio Service Mesh — mTLS automático

📚 Limpeza do Ambiente

🐧 Linux / Mac e ⊞ Windows (mesmo comando):

terraform destroy

Isso destrói o cluster inteiro na DigitalOcean. Confirme com yes quando solicitado.