DevSecOps: Integrando Segurança em Todo o Pipeline

[310] DevSecOps: Integrando Segurança em Todo o Pipeline

Segurança deslocada para a esquerda, em seis camadas: análise estática com Semgrep e detecção de segredos com Gitleaks, auditoria de dependências com Renovate, scanning de imagens com Trivy, infraestrutura verificada por Checkov, detecção em runtime com Falco e segredos dinâmicos no Vault.
DevOps

24 min de leitura

Durante décadas, a segurança foi tratada como uma fase — algo que acontecia depois que o software estava pronto, executado por um time separado que revisava o código antes do release. Esse modelo produziu dois problemas persistentes: vulnerabilidades encontradas tarde no ciclo de desenvolvimento são ordens de grandeza mais caras de corrigir, e times de segurança operando como gargalhos de aprovação inevitavelmente ficaram para trás da velocidade de entrega que o DevOps trouxe.

O DevSecOps é a resposta a esses dois problemas. O princípio central é deslocar a segurança para a esquerda — shift left — integrando verificações de segurança em cada etapa do ciclo de desenvolvimento, do editor de código ao ambiente de produção. Não como um gargalo, mas como feedback automático que o desenvolvedor recebe no mesmo fluxo em que recebe feedback de testes.

A segurança eficaz em pipelines modernos opera em camadas: código-fonte, dependências, imagens de container, infraestrutura como código, configurações do cluster e comportamento em runtime. Este artigo percorre cada uma dessas camadas com as ferramentas e práticas que as tornam automatizáveis.

Camada 1: Segurança no Código-Fonte

Análise Estática com Semgrep

O Semgrep é um analisador estático de código que detecta padrões de segurança diretamente no código-fonte — injeção SQL, segredos hardcoded, uso de funções inseguras, lógica de autenticação defeituosa. Opera sobre o AST do código, não sobre texto, o que elimina falsos positivos comuns em ferramentas baseadas em regex:

# .github/workflows/segurança-código.yml
name: Análise de Segurança do Código

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  semgrep:
    runs-on: ubuntu-latest
    container:
      image: returntocorp/semgrep:latest

    steps:
      - uses: actions/checkout@v4

      - name: Executa Semgrep
        run: |
          semgrep scan \
            --config=p/nodejs \
            --config=p/javascript \
            --config=p/secrets \
            --config=p/owasp-top-ten \
            --error \
            --json \
            --output=semgrep-results.json \
            --metrics=off \
            .

      - name: Publica resultados como SARIF
        if: always()
        run: |
          semgrep scan \
            --config=p/nodejs \
            --config=p/secrets \
            --sarif \
            --output=semgrep.sarif \
            --metrics=off \
            .

      - name: Upload resultados para GitHub Security
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: semgrep.sarif

Regras customizadas do Semgrep para padrões específicos do projeto:

# .semgrep/regras-customizadas.yml
rules:
  # Detecta queries SQL construídas por concatenação de strings
  - id: sql-injection-concatenacao
    patterns:
      - pattern: |
          $QUERY = "..." + $USER_INPUT
      - pattern-either:
          - pattern: db.query($QUERY, ...)
          - pattern: pool.query($QUERY, ...)
    message: |
      Query SQL construída por concatenação detectada.
      Use queries parametrizadas: db.query('SELECT * FROM users WHERE id = $1', [id])
    languages: [javascript, typescript]
    severity: ERROR

  # Detecta uso de algoritmos de hash inseguros para senhas
  - id: hash-inseguro-para-senha
    patterns:
      - pattern-either:
          - pattern: crypto.createHash('md5')
          - pattern: crypto.createHash('sha1')
          - pattern: crypto.createHash('sha256')
      - pattern-not-inside: |
          # Ignora uso legítimo para checksums de arquivos
          fs.$READ(...)
    message: |
      Algoritmo de hash inseguro para senhas.
      Use bcrypt, argon2 ou scrypt para hashing de senhas.
    languages: [javascript, typescript]
    severity: ERROR

  # Detecta JWT sem verificação de algoritmo
  - id: jwt-algorithm-none
    pattern: |
      jwt.verify($TOKEN, $SECRET, { algorithms: [..., "none", ...] })
    message: |
      O algoritmo "none" no JWT permite tokens não assinados.
      Remova "none" da lista de algoritmos permitidos.
    languages: [javascript, typescript]
    severity: ERROR

  # Detecta logging de informações sensíveis
  - id: log-dados-sensiveis
    patterns:
      - pattern-either:
          - pattern: console.log(..., $REQ.body.password, ...)
          - pattern: logger.info(..., $REQ.body.password, ...)
          - pattern: console.log(..., $REQ.headers.authorization, ...)
    message: |
      Dados sensíveis sendo logados.
      Nunca logue senhas, tokens ou dados de cartão.
    languages: [javascript, typescript]
    severity: WARNING

Detecção de Segredos com Gitleaks

O Gitleaks detecta segredos — chaves de API, tokens, senhas — no histórico do Git, incluindo commits antigos:

# .github/workflows/detectar-segredos.yml
  gitleaks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Histórico completo para escanear todos os commits

      - name: Executa Gitleaks
        uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Configuração para reduzir falsos positivos:

# .gitleaks.toml
title = "Configuração Gitleaks"

[extend]
useDefault = true

[[allowlist]]
description = "Ignora arquivos de teste com dados falsos"
paths = [
  '''tests/fixtures/.*''',
  '''\.env\.example''',
  '''\.env\.test''',
]

[[allowlist]]
description = "Ignora padrões de placeholder"
regexes = [
  '''EXAMPLE_KEY_.*''',
  '''your[-_]api[-_]key''',
  '''<YOUR_.*>''',
  '''TODO.*key''',
]

Camada 2: Segurança em Dependências

Software Composition Analysis com npm audit e Snyk

Aplicações modernas dependem de centenas de pacotes de terceiros. Cada pacote é uma superfície de ataque potencial — e vulnerabilidades em dependências transitivas são frequentemente invisíveis até que causem um incidente.

# No pipeline de CI — verificação de dependências
  dependencias:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Instala dependências
        run: npm ci

      - name: Auditoria de segurança npm
        run: |
          # Falha se houver vulnerabilidades de alta criticidade
          npm audit --audit-level=high --json > audit-results.json || {
            echo "Vulnerabilidades críticas encontradas:"
            cat audit-results.json | jq '.vulnerabilities | to_entries[] | 
              select(.value.severity == "high" or .value.severity == "critical") |
              {pacote: .key, severidade: .value.severity, 
               CVE: .value.via[0].url}'
            exit 1
          }

      - name: Snyk para análise aprofundada
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        with:
          args: >
            --severity-threshold=high
            --fail-on=upgradable
            --sarif-file-output=snyk.sarif

      - name: Upload resultados Snyk
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: snyk.sarif

Renovate: Atualizações Automáticas de Dependências

O Renovate abre pull requests automaticamente quando novas versões de dependências são publicadas, incluindo informações de changelog e resultados de testes:

// renovate.json
{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "config:recommended",
    "security:openssf-scorecard"
  ],
  "schedule": ["before 6am on Monday"],
  "timezone": "America/Sao_Paulo",
  "labels": ["dependências", "automated"],
  "prConcurrentLimit": 10,
  "packageRules": [
    {
      "matchUpdateTypes": ["patch"],
      "automerge": true,
      "automergeType": "pr",
      "platformAutomerge": true
    },
    {
      "matchUpdateTypes": ["major"],
      "dependencyDashboardApproval": true
    },
    {
      "matchPackageNames": ["express", "pg", "redis"],
      "reviewers": ["time-backend"]
    }
  ],
  "vulnerabilityAlerts": {
    "labels": ["segurança", "vulnerabilidade"],
    "automerge": true
  }
}

Camada 3: Segurança em Imagens de Container

Trivy: Scanning de Vulnerabilidades em Imagens

O Trivy escaneia imagens de container em busca de vulnerabilidades no sistema operacional base, bibliotecas de linguagens e configurações inseguras:

# Etapa de scanning no pipeline de build de imagem
  scanning-imagem:
    runs-on: ubuntu-latest
    needs: build

    steps:
      - name: Escaneia imagem com Trivy
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ghcr.io/empresa/minha-api:${{ github.sha }}
          format: sarif
          output: trivy-results.sarif
          severity: CRITICAL,HIGH
          exit-code: '1'
          ignore-unfixed: true

      - name: Upload resultados Trivy
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: trivy-results.sarif

Dockerfile Seguro

# Dockerfile com práticas de segurança integradas

# Stage 1: Build
FROM node:20-alpine AS builder

WORKDIR /app

# Instala dependências antes de copiar o código
# Aproveita cache de camadas do Docker
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force

COPY . .
RUN npm run build

# Stage 2: Imagem final mínima
FROM node:20-alpine AS runtime

# Instala apenas o necessário para runtime
RUN apk add --no-cache \
    dumb-init \
    curl \
  && rm -rf /var/cache/apk/*

# Cria usuário não-root
RUN addgroup -g 1001 -S nodejs \
  && adduser -S nodejs -u 1001 -G nodejs

WORKDIR /app

# Copia apenas os artefatos necessários
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./

# Garante que o binário pertence ao root mas é executável pelo usuário
RUN chown -R nodejs:nodejs /app

USER nodejs

EXPOSE 3000

# Health check integrado na imagem
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

# dumb-init garante propagação correta de sinais
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/server.js"]

Camada 4: Segurança em Infraestrutura como Código

Checkov: Analisando Terraform e Kubernetes

O Checkov verifica arquivos de IaC em busca de configurações inseguras — buckets S3 públicos, security groups com 0.0.0.0/0, pods sem limites de recursos, containers rodando como root:

  checkov:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Executa Checkov no Terraform
        uses: bridgecrewio/checkov-action@master
        with:
          directory: terraform/
          framework: terraform
          output_format: sarif
          output_file_path: checkov-terraform.sarif
          skip_check: >
            CKV_AWS_136,
            CKV2_AWS_62

      - name: Executa Checkov nos manifestos Kubernetes
        uses: bridgecrewio/checkov-action@master
        with:
          directory: kubernetes/
          framework: kubernetes
          output_format: sarif
          output_file_path: checkov-k8s.sarif

      - name: Upload resultados
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: checkov-terraform.sarif

Arquivo de supressão documentada para exceções legítimas:

# .checkov-suppressions.yml
# Supressões documentadas de verificações do Checkov
# Cada supressão deve ter justificativa e data de revisão

suppressions:
  - check_id: CKV_AWS_18
    resource: aws_s3_bucket.cloudfront_logs
    justification: >
      Bucket de logs do CloudFront não precisa de access logging próprio
      — logging circular não adiciona valor e aumenta custos.
    revisar_em: "2025-06-01"
    responsavel: time-infra

  - check_id: CKV_K8S_8
    resource: Deployment/cert-manager
    justification: >
      cert-manager requer capabilities específicas para gerenciar
      certificados TLS no cluster. Exceção aprovada pelo time de segurança.
    revisar_em: "2025-06-01"
    responsavel: joao-silva

Camada 5: Runtime Security com Falco

As verificações anteriores são preventivas — detectam problemas antes do deploy. O Falco complementa a postura de segurança com detecção em runtime: monitora syscalls no kernel e gera alertas quando comportamentos suspeitos são detectados em produção.

# kubernetes/falco/falco-rules.yml
customRules:
  regras-empresa.yaml: |-
    # Detecta execução de shell dentro de containers de aplicação
    - rule: Shell em container de aplicação
      desc: Detecta abertura de shell interativo em containers de aplicação
      condition: >
        spawned_process and
        container and
        proc.name in (shell_binaries) and
        not proc.pname in (runc, containerd) and
        k8s.ns.name = "producao"
      output: >
        Shell iniciado em container de produção
        (usuario=%user.name container=%container.name
         imagem=%container.image.repository:%container.image.tag
         pod=%k8s.pod.name namespace=%k8s.ns.name
         comando=%proc.cmdline)
      priority: CRITICAL
      tags: [container, shell, T1059]

    # Detecta leitura de arquivos sensíveis
    - rule: Leitura de arquivo sensível em container
      desc: Detecta tentativa de leitura de /etc/shadow ou /etc/passwd
      condition: >
        open_read and
        container and
        fd.name in (/etc/shadow, /etc/passwd, /etc/ssh/ssh_host_rsa_key) and
        not proc.name in (sshd, passwd, shadow)
      output: >
        Arquivo sensível lido em container
        (arquivo=%fd.name processo=%proc.name
         pod=%k8s.pod.name namespace=%k8s.ns.name)
      priority: WARNING
      tags: [filesystem, credentials]

    # Detecta conexões de rede inesperadas
    - rule: Conexão de rede inesperada em container
      desc: Detecta containers abrindo conexões para portas não padronizadas
      condition: >
        outbound and
        container and
        k8s.ns.name = "producao" and
        not fd.sport in (80, 443, 5432, 6379, 53) and
        not proc.name in (node, npm)
      output: >
        Conexão de rede inesperada
        (processo=%proc.name porta_destino=%fd.sport
         ip_destino=%fd.rip pod=%k8s.pod.name)
      priority: WARNING
      tags: [network, lateral-movement]

Camada 6: Gestão de Segredos com HashiCorp Vault

Para organizações com requisitos mais rigorosos de gestão de segredos — rotação automática, acesso auditado, segredos dinâmicos — o HashiCorp Vault oferece capacidades que vão além do AWS Secrets Manager:

# vault.tf — configuração do Vault no Kubernetes

resource "helm_release" "vault" {
  name             = "vault"
  repository       = "https://helm.releases.hashicorp.com"
  chart            = "vault"
  namespace        = "vault"
  create_namespace = true
  version          = "0.27.0"

  values = [yamlencode({
    server = {
      ha = {
        enabled  = true
        replicas = 3
        raft = {
          enabled   = true
          setNodeId = true
        }
      }

      auditStorage = {
        enabled      = true
        storageClass = "gp3"
        size         = "10Gi"
      }

      dataStorage = {
        enabled      = true
        storageClass = "gp3"
        size         = "20Gi"
      }
    }

    injector = {
      enabled = true
    }
  })]
}

Configurando segredos dinâmicos para o banco de dados — o Vault gera credenciais temporárias com tempo de vida limitado:

# Configura o backend de banco de dados no Vault
vault secrets enable database

vault write database/config/minha-api-db \
  plugin_name=postgresql-database-plugin \
  allowed_roles="minha-api-producao" \
  connection_url="postgresql://{{username}}:{{password}}@db.interno.empresa.com:5432/producao" \
  username="vault_root" \
  password="senha_root_vault"

# Define uma role que gera credenciais com TTL de 1 hora
vault write database/roles/minha-api-producao \
  db_name=minha-api-db \
  creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
  revocation_statements="REVOKE ALL ON ALL TABLES IN SCHEMA public FROM \"{{name}}\"; DROP ROLE IF EXISTS \"{{name}}\";" \
  default_ttl="1h" \
  max_ttl="24h"

O Vault Agent Injector injeta as credenciais geradas diretamente no pod como variáveis de ambiente:

# Annotations no Deployment para injeção do Vault Agent
spec:
  template:
    metadata:
      annotations:
        vault.hashicorp.com/agent-inject: "true"
        vault.hashicorp.com/role: "minha-api-producao"
        vault.hashicorp.com/agent-inject-secret-db-creds: "database/creds/minha-api-producao"
        vault.hashicorp.com/agent-inject-template-db-creds: |
          {{- with secret "database/creds/minha-api-producao" -}}
          DATABASE_URL=postgresql://{{ .Data.username }}:{{ .Data.password }}@db.interno.empresa.com:5432/producao
          {{- end }}

Pipeline de Segurança Completo

A integração de todas as camadas em um único workflow garante que nenhuma etapa é pulada:

# .github/workflows/pipeline-segurança.yml
name: Pipeline de Segurança

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 2 * * 1'  # Toda segunda-feira às 2h — scan completo

jobs:
  segredos:
    name: Detectar Segredos
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  sast:
    name: Análise Estática (SAST)
    runs-on: ubuntu-latest
    needs: segredos
    steps:
      - uses: actions/checkout@v4
      - name: Semgrep
        uses: returntocorp/semgrep-action@v1
        with:
          config: >
            p/nodejs
            p/secrets
            p/owasp-top-ten
            .semgrep/regras-customizadas.yml

  sca:
    name: Análise de Composição (SCA)
    runs-on: ubuntu-latest
    needs: segredos
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm audit --audit-level=high
      - uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        with:
          args: --severity-threshold=high

  container:
    name: Segurança da Imagem
    runs-on: ubuntu-latest
    needs: [sast, sca]
    steps:
      - uses: actions/checkout@v4
      - name: Constrói imagem
        run: docker build -t minha-api:scan .
      - name: Trivy
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: minha-api:scan
          severity: CRITICAL,HIGH
          exit-code: '1'
          ignore-unfixed: true

  iac:
    name: Segurança da Infraestrutura
    runs-on: ubuntu-latest
    needs: segredos
    steps:
      - uses: actions/checkout@v4
      - uses: bridgecrewio/checkov-action@master
        with:
          directory: .
          framework: terraform,kubernetes,dockerfile

  relatorio:
    name: Consolida Relatório de Segurança
    runs-on: ubuntu-latest
    needs: [sast, sca, container, iac]
    if: always()
    steps:
      - name: Verifica status de todos os jobs
        run: |
          if [[ "${{ needs.sast.result }}" == "failure" ]] || \
             [[ "${{ needs.sca.result }}" == "failure" ]] || \
             [[ "${{ needs.container.result }}" == "failure" ]] || \
             [[ "${{ needs.iac.result }}" == "failure" ]]; then
            echo "::error::Falhas de segurança detectadas. Revisão obrigatória antes do merge."
            exit 1
          fi
          echo "Todas as verificações de segurança passaram."

Métricas de Maturidade em DevSecOps

A maturidade de uma postura DevSecOps pode ser medida por indicadores concretos que evoluem ao longo do tempo:

Mean Time to Remediation (MTTR) de vulnerabilidades — quanto tempo leva desde a detecção de uma vulnerabilidade até a correção em produção. Times maduros medem isso em horas para criticalidades altas, não semanas.

Cobertura de scanning — percentual de repositórios, imagens e configurações de IaC cobertos por verificações automatizadas. O objetivo é 100% — sem exceções não documentadas.

Taxa de falsos positivos — verificações que geram muito ruído são ignoradas pelos desenvolvedores. Manter a taxa de falsos positivos baixa é tão importante quanto a cobertura.

Vulnerabilidades por categoria e origem — rastrear de onde vêm as vulnerabilidades (código próprio vs dependências vs configuração) direciona onde investir esforço de melhoria.

O Que Vem a Seguir

O próximo artigo completa a visão de segurança com compliance e auditoria — como mapear controles técnicos para frameworks regulatórios como LGPD, SOC2 e ISO 27001, como implementar logging de auditoria imutável e como preparar evidências de conformidade de forma automatizada.

Referências para Aprofundamento

Ferramentas

Frameworks e referências

Exercícios

Exercício 1

O primeiro estágio do Dockerfile executa, nesta ordem, npm ci --only=production e depois npm run build. O build falha com sh: tsc: not found (ou vite: not found, conforme a stack). Explique a causa e escreva a correção — sem abrir mão de uma imagem final enxuta.

Ver resposta

✓ Resposta: A flag --only=production instala apenas as dependencies, pulando as devDependencies. E é exatamente lá que moram as ferramentas de build: TypeScript, Vite, webpack, esbuild, Babel. O npm run build tenta executar um binário que nunca foi instalado.

A confusão é sobre o papel de cada estágio: o builder precisa de tudo; quem precisa ser enxuto é a imagem final. Isso pede três estágios, com um deles dedicado a produzir um node_modules de produção limpo:

# Stage 1: dependências de produção — só o que vai para a imagem final
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force

# Stage 2: build — precisa das devDependencies
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 3: runtime
FROM node:20-alpine AS runtime
# ...
COPY --from=deps    --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist

Repare que o COPY do node_modules passa a vir do estágio deps, não do builder — do contrário toda a cadeia de devDependencies seria copiada para a imagem final, inflando o tamanho e, o que importa mais aqui, ampliando a superfície de scan do Trivy com pacotes que nem sequer rodam em produção.

Um detalhe de manutenção: --only=production está obsoleto desde o npm 7. A grafia atual é --omit=dev. A antiga ainda funciona, mas encontrá-la costuma ser sinal de um Dockerfile copiado de um tutorial antigo — vale conferir o resto do arquivo com o mesmo olhar.

Exercício 2

A regra hash-inseguro-para-senha lista md5, sha1 e sha256 como algoritmos a bloquear. Mas SHA-256 é considerado criptograficamente seguro e é usado, por exemplo, na deduplicação de arquivos e em assinaturas digitais. Por que ele aparece nessa lista?

Ver resposta

✓ Resposta: Porque o critério para senhas não é o mesmo. MD5 e SHA-1 estão na lista por serem quebrados — há colisões práticas conhecidas. SHA-256 está por um motivo diferente: ele é rápido demais.

Um hash de propósito geral é projetado para ser veloz, e isso é uma virtude em checksum de arquivo ou verificação de integridade. Para senha é o oposto do que se quer: uma GPU moderna calcula bilhões de SHA-256 por segundo, então um vazamento do banco vira um ataque de dicionário viável contra a maioria das senhas reais. Algoritmos como bcrypt, scrypt e Argon2 foram desenhados para serem deliberadamente lentos, com custo configurável — o fator de trabalho pode ser elevado conforme o hardware evolui, sem trocar de algoritmo. Argon2 e scrypt vão além e exigem memória, o que reduz a vantagem de paralelizar em GPU. Eles também embutem salt por senha, enquanto crypto.createHash('sha256') não tem salt algum — senhas iguais produzem hashes iguais, e uma rainbow table resolve todas de uma vez.

Isso explica o pattern-not-inside da regra: a intenção é liberar o uso legítimo de SHA-256 para checksum de arquivo e barrar o uso em senha. Vale notar que essa exclusão é frágil — casar apenas fs.$READ(...) deixa passar muitos usos legítimos (streams, buffers em memória, hash de payload de webhook) e ainda assim não distingue com segurança a intenção do código. É a tensão central de qualquer SAST: uma regra rígida demais vira ruído e acaba desligada; frouxa demais não protege. Na prática, a regra mais confiável não procura o algoritmo, e sim o contexto — variáveis chamadas password, senha, pwd chegando a qualquer função de hash.

Exercício 3

O passo do Trivy usa severity: CRITICAL,HIGH, exit-code: '1' e ignore-unfixed: true. Explique o que a última flag faz e por que ela é, ao mesmo tempo, necessária e perigosa.

Ver resposta

✓ Resposta: ignore-unfixed: true faz o Trivy reportar apenas vulnerabilidades que já têm correção disponível — ou seja, aquelas em que existe uma versão corrigida do pacote. CVEs conhecidos mas ainda sem patch publicado pelo mantenedor da distribuição são omitidos do resultado.

É necessária por uma razão puramente operacional: sem ela, o build passa a falhar por vulnerabilidades que ninguém pode corrigir. Uma CVE crítica numa biblioteca base do Alpine, ainda sem patch, quebraria todos os deploys da empresa por dias — e a reação previsível do time seria desligar o scan ou adicionar um continue-on-error. Uma verificação que bloqueia sem oferecer caminho de saída acaba sendo removida, e o resultado líquido é menos segurança.

É perigosa porque cria um ponto cego exatamente onde a atenção é mais necessária. Um zero-day ativamente explorado, sem patch, é o cenário de maior risco — e é precisamente o que essa flag esconde. O pipeline fica verde enquanto a imagem carrega uma vulnerabilidade explorável, e a equipe não sabe que ela existe.

A saída não é escolher entre as duas coisas, e sim separar bloquear de enxergar: manter ignore-unfixed no passo que decide o merge, e rodar um scan completo — sem a flag, sem exit-code — publicando o resultado no GitHub Security. O schedule semanal já presente no workflow é o lugar natural para isso: imagens já publicadas continuam sendo reavaliadas contra o banco de CVEs atualizado, e uma vulnerabilidade que ganhou correção durante a semana aparece sem depender de alguém abrir um PR.

Exercício 4

No pipeline-segurança.yml, um desenvolvedor commita por engano uma chave da AWS. O job segredos falha. Percorra o que acontece com os demais jobs e diga o que o job relatorio vai imprimir e com que código de saída.

Ver resposta

✓ Resposta: O relatorio imprime "Todas as verificações de segurança passaram." e sai com código 0.

A cadeia é esta: sast, sca e iac declaram needs: segredos, então a falha faz os três serem pulados; container depende de [sast, sca] e é pulado junto. O relatorio roda porque tem if: always(), e testa apenas needs.<job>.result == "failure" nos quatro. Só que o resultado deles não é failure — é skipped. Nenhuma comparação bate, o if não dispara, e o job termina anunciando sucesso. Note ainda que segredos nem sequer está na lista de needs do relatório: o único job que efetivamente falhou não é consultado.

O workflow inteiro ainda aparece vermelho no GitHub, porque um job falhou. O problema é quando a proteção de branch exige o check "Consolida Relatório de Segurança" como required — que é justamente o motivo de existir um job consolidador. Aí o merge é liberado por um relatório que afirma que tudo passou.

A correção é inverter a lógica: em vez de procurar por failure, exigir success — assim skipped, cancelled e qualquer estado futuro reprovam por padrão:

  relatorio:
    needs: [segredos, sast, sca, container, iac]
    if: always()
    steps:
      - name: Verifica status de todos os jobs
        run: |
          if [[ "${{ contains(needs.*.result, 'success') }}" == "true" && \
                "${{ !contains(needs.*.result, 'failure') }}" == "true" && \
                "${{ !contains(needs.*.result, 'skipped') }}" == "true" && \
                "${{ !contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
            echo "Todas as verificações de segurança passaram."
          else
            echo "::error::Falhas de segurança detectadas. Revisão obrigatória antes do merge."
            exit 1
          fi

A lição geral vale além deste arquivo: um portão de qualidade que só reage a uma falha explícita falha em aberto. O estado seguro por padrão é reprovar, e liberar apenas mediante confirmação positiva de que cada etapa realmente executou e passou.

Exercício 5

Observe o comando vault write database/config/minha-api-db. Ele configura o Vault para gerar credenciais temporárias de banco — mas há um problema de segurança no próprio comando. Identifique-o e diga como o Vault resolve isso.

Ver resposta

✓ Resposta: A credencial mais poderosa de todo o arranjo — username="vault_root" com password="senha_root_vault" — é passada em texto puro, como argumento de linha de comando. Essa é a conta que pode criar e destruir roles no PostgreSQL; quem a obtém não precisa do Vault para nada.

Argumentos de comando vazam por vários caminhos ao mesmo tempo: ficam no ~/.bash_history, aparecem para qualquer usuário da máquina em um ps aux, são gravados em logs de auditoria de shell e, se o comando estiver num script versionado, entram no histórico do Git. Há uma ironia estrutural aqui: monta-se toda uma infraestrutura de segredos dinâmicos para eliminar credenciais estáticas de longa duração, e o passo de configuração deixa a mais crítica delas exposta.

O Vault resolve isso com rotação da credencial raiz. Logo após a configuração inicial, executa-se:

vault write -force database/rotate-root/minha-api-db

O Vault troca a senha de vault_root no PostgreSQL por um valor aleatório que ele mesmo passa a guardar e ninguém jamais conhece — nem o operador que rodou o comando. A senha que vazou no histórico deixa de valer no instante da rotação. A partir daí, não existe mais nenhum ser humano capaz de autenticar como vault_root; o acesso ao banco passa exclusivamente pelas credenciais temporárias de 1 hora, cada uma emitida e revogada com registro em auditoria.

Nota prática: depois da rotação, só o Vault consegue administrar aquele usuário. Se o servidor Vault for perdido sem backup, a recuperação é manual, direto no PostgreSQL com uma conta de superusuário separada — motivo pelo qual o auditStorage e o dataStorage em modo HA com Raft, configurados no helm_release acima, não são opcionais.

Comentários

Mais em DevOps

Processos, Serviços e o Comando `systemctl`
Processos, Serviços e o Comando `systemctl`

Processos e serviços no Linux: leitura de ps aux e top, busca com pgrep, a…

Permissões, Usuários e Grupos no Linux
Permissões, Usuários e Grupos no Linux

O sistema de permissões do Linux como mecanismo de isolamento entre serviços…

RDS, ElastiCache e Estratégias de Dados na AWS
RDS, ElastiCache e Estratégias de Dados na AWS

A camada mais crítica e mais difícil de recuperar quando algo dá errado: RDS…