DEVSECOPS 12 min

Pipeline DevSecOps en GitHub Actions: Docker y SonarQube

Cierra la brecha entre desarrollo y operaciones automatizando la compilación, el análisis estático de seguridad y la publicación de imágenes en registros de contenedores.

Escrito por Samir El Azem Crovetto • Senior Backend Engineer & DevSecOps

El Imperativo del Shift-Left: La Seguridad Integrada en la Entrega Continua

Históricamente, la seguridad del software se delegaba a una fase de auditoría o prueba de penetración (pentesting) previa al paso a producción. Este enfoque reactivo generaba cuellos de botella insostenibles: los equipos de desarrollo acumulaban semanas de deuda técnica y corregir una vulnerabilidad de inyección o una dependencia desactualizada a escasos días del lanzamiento implicaba costosas refactorizaciones de emergencia.

El paradigma DevSecOps transforma este flujo mediante la filosofía Shift-Left (desplazar la seguridad hacia la izquierda del ciclo de vida), como analizamos en qué es DevSecOps. Cada commit, pull request y compilación se somete a una batería automatizada de análisis estático, detección de secretos y verificación de dependencias dentro del pipeline de integración continua (CI/CD).

En este ecosistema, la combinación de GitHub Actions como motor de orquestación, Docker para la estandarización de artefactos y SonarQube como plataforma de análisis estático de código fuente (Static Application Security Testing - SAST) constituye una de las arquitecturas más robustas para garantizar que ningún código defectuoso o vulnerable alcance el entorno de producción.


Arquitectura del Pipeline DevSecOps: De la Pull Request al Registro de Contenedores

Un pipeline CI/CD seguro se estructura en etapas progresivas con puertas de calidad (Quality Gates) infranqueables:

graph TD
    subgraph Developer_Stage ["👨‍💻 Desarrollo"]
        Commit["Git Commit & Push"]
        PR["Apertura de Pull Request"]
    end

    subgraph CI_Pipeline ["⚙️ Pipeline GitHub Actions"]
        subgraph Stage_1 ["Etapa 1: Detección Temprana"]
            SecretScan["Escaneo de Secretos: Gitleaks"]
            Linting["Linter de Código & Tipos"]
        end

        subgraph Stage_2 ["Etapa 2: SAST & Calidad"]
            Build["Compilación & Tests Unitarios"]
            SonarScan["SonarQube Scanner (SAST)"]
            QualityGate{"Quality Gate Evaluado"}
        end

        subgraph Stage_3 ["Etapa 3: Contenedores & SCA"]
            DockerBuild["Docker Multi-Stage Build"]
            TrivyScan["Escaneo de Vulnerabilidades (Trivy)"]
            ImageGate{"CVEs Críticos <= 0"}
        end
    end

    subgraph Release_Stage ["🚀 Entrega y Despliegue"]
        PushRegistry["Publicación en Registro (GHCR / ECR)"]
        Deploy["Despliegue Continuo (Kubernetes / ECS)"]
    end

    Commit --> PR
    PR --> SecretScan
    SecretScan --> Linting
    Linting --> Build
    Build --> SonarScan
    SonarScan --> QualityGate
    QualityGate -->|Fallo: Bloqueo de PR| Developer_Stage
    QualityGate -->|Aprobado| DockerBuild
    DockerBuild --> TrivyScan
    TrivyScan --> ImageGate
    ImageGate -->|Fallo: Imagen Rechazada| Developer_Stage
    ImageGate -->|Aprobado| PushRegistry
    PushRegistry --> Deploy

    classDef dev fill:#e1f5fe,stroke:#0288d1,stroke-width:2px;
    classDef ci fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px;
    classDef gate fill:#fff3e0,stroke:#f57c00,stroke-width:2px;
    classDef release fill:#e8f5e9,stroke:#388e3c,stroke-width:2px;

    class Commit,PR dev;
    class SecretScan,Linting,Build,SonarScan,DockerBuild,TrivyScan ci;
    class QualityGate,ImageGate gate;
    class PushRegistry,Deploy release;

Despliegue de SonarQube Autohospedado con Docker Compose y PostgreSQL

Para organizaciones que gestionan código propietario en repositorios privados y prefieren mantener el análisis estático dentro de su propia infraestructura, desplegar SonarQube mediante Docker Compose sobre una base de datos PostgreSQL dedicada es la solución estándar.

Requisitos Previos del Sistema Operativo Host:

SonarQube utiliza internamente una instancia embebida de Elasticsearch para indexar reglas y análisis. El kernel de Linux del host debe ajustarse para permitir la memoria virtual requerida por Elasticsearch:

# Aplicar parámetros en el host de producción
sudo sysctl -w vm.max_map_count=262144
sudo sysctl -w fs.file-max=131072

# Persistir en /etc/sysctl.conf
echo "vm.max_map_count=262144" | sudo tee -a /etc/sysctl.conf
echo "fs.file-max=131072" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

Fichero docker-compose.yml Hardened para Producción:

Siguiendo las directivas de seguridad en contenedores vistas en Docker para bases de datos relacionales, se aíslan las redes internas y se emplean volúmenes persistentes nombrados:

version: '3.8'

services:
  sonarqube-db:
    image: postgres:16-alpine
    container_name: sonarqube-db
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${SONAR_DB_USER:-sonar}
      POSTGRES_PASSWORD: ${SONAR_DB_PASSWORD:?Error: Contraseña de DB requerida}
      POSTGRES_DB: ${SONAR_DB_NAME:-sonar}
    volumes:
      - postgresql_data:/var/lib/postgresql/data
    networks:
      - sonarqube-internal
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U sonar"]
      interval: 10s
      timeout: 5s
      retries: 5

  sonarqube:
    image: sonarqube:10.5-community
    container_name: sonarqube-server
    restart: unless-stopped
    depends_on:
      sonarqube-db:
        condition: service_healthy
    environment:
      SONAR_JDBC_USERNAME: ${SONAR_DB_USER:-sonar}
      SONAR_JDBC_PASSWORD: ${SONAR_DB_PASSWORD:?Error: Contraseña requerida}
      SONAR_JDBC_URL: jdbc:postgresql://sonarqube-db:5432/sonar
      SONAR_SEARCH_JAVAADDITIONALOPTS: "-Dnode.store.allow_mmap=true"
    volumes:
      - sonarqube_data:/opt/sonarqube/data
      - sonarqube_extensions:/opt/sonarqube/extensions
      - sonarqube_logs:/opt/sonarqube/logs
    ports:
      # Vincular estrictamente a localhost para forzar paso por proxy reverso HTTPS
      - "127.0.0.1:9000:9000"
    networks:
      - sonarqube-internal
      - sonarqube-public
    ulimits:
      nofile:
        soft: 131072
        hard: 131072

volumes:
  postgresql_data:
    driver: local
  sonarqube_data:
    driver: local
  sonarqube_extensions:
    driver: local
  sonarqube_logs:
    driver: local

networks:
  sonarqube-internal:
    internal: true # Sin acceso a Internet para la base de datos
  sonarqube-public:
    driver: bridge

Este diseño garantiza que la base de datos PostgreSQL permanezca en una red interna sin salida a Internet, mientras que SonarQube solo expone su puerto web a la interfaz local 127.0.0.1, protegido por un proxy reverso con TLS 1.3.


Implementación del Workflow en GitHub Actions: De la Detección al Quality Gate

A continuación se muestra un pipeline declarativo completo en .github/workflows/devsecops-pipeline.yml que orquesta los análisis de seguridad ante cada Pull Request:

name: "DevSecOps CI Pipeline"

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

permissions:
  contents: read
  pull-requests: write
  security-events: write

jobs:
  secret-scan:
    name: "1. Detección de Secretos (Gitleaks)"
    runs-on: ubuntu-latest
    steps:
      - name: "Checkout del Código Fuente"
        uses: actions/checkout@v4
        with:
          fetch-depth: 0 # Historial completo para auditar commits anteriores

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

  sast-analysis:
    name: "2. Análisis SAST & Quality Gate (SonarQube)"
    needs: secret-scan
    runs-on: ubuntu-latest
    steps:
      - name: "Checkout del Código Fuente"
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: "Configurar JDK 21"
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'
          cache: 'maven'

      - name: "Compilación y Análisis SonarQube"
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
          SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
        run: |
          mvn clean verify sonar:sonar \
            -Dsonar.projectKey=corp-backend-service \
            -Dsonar.projectName="Corporate Backend Service" \
            -Dsonar.qualitygate.wait=true

  container-security:
    name: "3. Hardening de Imagen Docker (Trivy)"
    needs: sast-analysis
    runs-on: ubuntu-latest
    steps:
      - name: "Checkout del Código Fuente"
        uses: actions/checkout@v4

      - name: "Construir Imagen Docker Local"
        run: |
          docker build -t corp-backend-service:${{ github.sha }} .

      - name: "Escanear Imagen con Trivy (CVE Scanner)"
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'corp-backend-service:${{ github.sha }}'
          format: 'table'
          exit-code: '1' # Falla el job si encuentra vulnerabilidades críticas
          ignore-unfixed: true
          severity: 'CRITICAL,HIGH'

Parámetro Crucial: sonar.qualitygate.wait=true

La directiva -Dsonar.qualitygate.wait=true obliga al runner de GitHub Actions a pausar la ejecución tras enviar el informe a SonarQube y esperar el veredicto del Quality Gate. Si el código introduce:

  • Cobertura de pruebas unitarias inferior al 80%.
  • Cualquier vulnerabilidad de severidad "Bloqueante" (Bugs o Vulnerabilities).
  • Calificación de seguridad inferior a A.
  • Código duplicado superior al 3%.

El comando finaliza con código de error 1, bloqueando automáticamente la fusión de la Pull Request en GitHub y notificando al desarrollador los fallos exactos en su código.


Prevención de Fugas de Secretos en CI/CD

Un vector habitual de compromiso en pipelines de entrega continua es la exposición inadvertida de credenciales de infraestructura (claves de acceso de AWS, tokens de SonarQube o contraseñas de bases de datos).

Para blindar el entorno:

  1. Evitar Claves de AWS de Larga Duración: En lugar de almacenar AWSACCESSKEYID y AWSSECRETACCESSKEY en los Secretos de GitHub, se debe utilizar OpenID Connect (OIDC) federado con AWS IAM (aws-actions/configure-aws-credentials). GitHub emite un token JWT efímero que AWS intercambia por credenciales STS temporales válidas por 1 hora.
  2. Uso de .dockerignore Estricto: Evitar que ficheros .env, claves privadas .pem o carpetas de configuración local .git se copien en el contexto de compilación de Docker.
  3. Escaneo Pre-Commit en Local: Configurar hooks de Git en los entornos de desarrollo con herramientas como pre-commit ejecutando Gitleaks antes de que el código salga de la máquina del programador.

Optimización de Rendimiento y Caché en Pipelines CI/CD

Un pipeline DevSecOps riguroso no debe convertirse en un cuello de botella para la agilidad del equipo de desarrollo. La ejecución reiterada de análisis estáticos, descargas de dependencias y compilaciones de contenedores puede incrementar los tiempos de feedback de minutos a decenas de minutos si no se implementa una estrategia de caché distribuida:

  1. Caché de Dependencias Maven/Gradle: Utilizar actions/cache o la directiva nativa de actions/setup-java (cache: 'maven') para almacenar el directorio ~/.m2/repository. Esto reduce la fase de compilación en más de un 70% al evitar descargas repetitivas desde Maven Central.
  2. Docker Buildx con Caché Remota en GitHub Actions: Emplear el backend de caché integrado de GitHub Actions (type=gha) para reutilizar capas intermedias del Dockerfile multi-stage entre distintas ejecuciones de la pull request:
   - name: "Compilar y Cachear Imagen Docker"
     uses: docker/build-push-action@v5
     with:
       context: .
       push: false
       tags: corp-backend-service:${{ github.sha }}
       cache-from: type=gha
       cache-to: type=gha,mode=max
  1. Gobernanza de Falsos Positivos en Trivy y SonarQube: Todo pipeline debe disponer de un mecanismo auditable para documentar excepciones temporales justificadas mediante archivos .trivyignore y reglas de exclusión en SonarQube, evitando que vulnerabilidades sin parche disponible (unfixed CVEs) o componentes en desuso bloqueen despliegues críticos sin una decisión explícita de riesgo aceptado.

Buenas Prácticas de Gobernanza en Pipelines Corporativos

Para maximizar la eficacia de un pipeline DevSecOps empresarial:

  • *Reglas de Protección de Ramas (Branch Protection Rules):* Exigir obligatoriamente que los jobs secret-scan, sast-analysis y container-security pasen en verde antes de permitir la fusión hacia main.
  • Inmutabilidad de Imágenes de Contenedores: Etiquetar las imágenes Docker con el hash SHA del commit de Git (${{ github.sha }}) o versiones SemVer estrictas, desaconsejando el uso de la etiqueta mutable :latest en producción.
  • Generación de SBOM (Software Bill of Materials): Incorporar herramientas como Syft o Trivy para generar un inventario CycloneDX o SPDX de todas las dependencias del contenedor, facilitando auditorías de cumplimiento normativo (como la Cyber Resilience Act europea).

Resumen y Checklist de Verificación en Producción

  • [x] SonarQube desplegado con PostgreSQL en red interna aislada y parámetros de kernel optimizados.
  • [x] Pipeline de GitHub Actions estructurado en etapas progresivas: secretos, SAST y análisis de contenedores.
  • [x] Quality Gate con espera síncrona (sonar.qualitygate.wait=true) configurado para bloquear Pull Requests.
  • [x] Escaneo de vulnerabilidades en imágenes Docker con Trivy bloqueando despliegues con CVEs críticos.
  • [x] Tokens y credenciales efímeras federadas mediante OIDC en lugar de claves de acceso estáticas.

Si necesitas auditar los pipelines de integración y despliegue continuo de tu organización, implementar análisis estático SAST o securizar registros de contenedores, puedes consultar mis Servicios de Ciberseguridad y Consultoría o ponerte en contacto conmigo a través de Sobre mí.