DevOps desde Cero: Seguridad y Hardening
Apoya este blog
Si te resulta util este contenido, considera apoyar el blog.
Introduccion
Bienvenido al articulo dieciocho de la serie DevOps desde Cero. Durante los ultimos diecisiete articulos construimos una aplicacion, la testeamos, la containerizamos, la deployeamos a Kubernetes, configuramos GitOps con ArgoCD, agregamos observabilidad y armamos un pipeline CI/CD completo. Todo funciona. Pero hay una pregunta que estuvimos esquivando todo este tiempo: algo de esto es seguro?
La seguridad no es una feature que agregas al final. Es una practica que tejes en cada capa de tu pipeline, tu infraestructura y tus habitos diarios. La buena noticia es que no necesitas ser un experto en seguridad para hacer bien lo basico. La mayoria de las brechas reales vienen de errores simples: credenciales filtradas, dependencias sin parchear, contenedores corriendo como root, accesos demasiado permisivos. Todo esto se puede prevenir con una checklist y algo de automatizacion.
Este articulo es intencionalmente una checklist para principiantes, no un deep dive. Si queres cobertura completa de temas como OPA Gatekeeper, Falco, o frameworks de policy-as-code, mira el articulo de SRE Security as Code. Para un recorrido detallado de RBAC de Kubernetes a nivel de API, consulta el RBAC Deep Dive. Aca nos vamos a enfocar en las cosas practicas que todo proyecto deberia hacer desde el dia uno.
Vamos a meternos de lleno.
La mentalidad shift-left en seguridad
El termino “shift left” significa mover los chequeos de seguridad mas temprano en el ciclo de desarrollo. En vez de descubrir una vulnerabilidad en produccion (o peor, despues de una brecha), la detectas durante el desarrollo o en tu pipeline de CI. Cuanto antes encontras un problema, mas barato y rapido es arreglarlo.
Pensalo asi. Si encontras un bug mientras escribis codigo, te toma cinco minutos arreglarlo. Si lo encontras en code review, te toma treinta minutos porque tenes que cambiar de contexto. Si lo encontras en staging, toma horas porque ahora QA esta involucrado. Si lo encontras en produccion, toma dias y puede involucrar un incidente. Los problemas de seguridad siguen la misma curva, excepto que las consecuencias son mas altas porque un problema de seguridad puede exponer los datos de tus usuarios.
Shift left no significa que dejas de hacer revisiones de seguridad en produccion. Significa que agregas chequeos automatizados en cada etapa para que las cosas obvias nunca lleguen tan lejos. Tu pipeline de CI se convierte en tu primera linea de defensa.
Las etapas del pipeline donde pertenecen los chequeos de seguridad:
- En el codigo: Linters, plugins de IDE, pre-commit hooks que detectan secretos hardcodeados o patrones inseguros antes de que hagas push
- Pull request: Herramientas SAST, scanners de dependencias y deteccion de secretos corren como checks de CI en cada PR
- Build time: Escaneo de imagenes de contenedores, generacion de SBOM, verificacion de imagenes base
- Deploy time: Admission controllers de Kubernetes, Pod Security Standards, enforcement de RBAC
- Runtime: Network policies, audit logging, deteccion de amenazas en runtime (cubierto en la serie SRE)
El resto de este articulo recorre cada una de estas etapas con ejemplos practicos que podes agregar a tu proyecto hoy.
SAST: Static Application Security Testing
Las herramientas SAST analizan tu codigo fuente sin ejecutarlo. Buscan patrones que se sabe que causan problemas de seguridad: inyeccion SQL, cross-site scripting (XSS), inyeccion de comandos, criptografia insegura, credenciales hardcodeadas y mas.
Lo clave que tenes que entender es que SAST no encuentra todos los bugs. Encuentra patrones comunes que matchean con firmas de vulnerabilidades conocidas. Pensalo como un corrector ortografico para seguridad. Atrapa los errores obvios para que puedas enfocar tu tiempo de revision manual en los sutiles.
Semgrep es una de las mejores herramientas para esto. Es open source, soporta muchos lenguajes, y tiene una biblioteca enorme de reglas de la comunidad. Tambien podes escribir tus propias reglas para patrones especificos de tu codebase.
Asi se agrega Semgrep a tu pipeline de GitHub Actions:
# .github/workflows/security.yml
name: Security Checks
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
sast:
name: SAST Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Semgrep
uses: semgrep/semgrep-action@v1
with:
config: >-
p/security-audit
p/secrets
p/owasp-top-ten
env:
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
Los p/security-audit, p/secrets y p/owasp-top-ten son paquetes de reglas que cubren los
patrones de vulnerabilidades mas comunes. Semgrep va a escanear tu codigo y reportar cualquier
coincidencia como comentarios en tu pull request.
Para proyectos JavaScript y TypeScript, tambien deberias agregar plugins de seguridad de ESLint:
npm install --save-dev eslint-plugin-security eslint-plugin-no-secrets
{
"plugins": ["security", "no-secrets"],
"extends": ["plugin:security/recommended"],
"rules": {
"no-secrets/no-secrets": "error",
"security/detect-eval-with-expression": "error",
"security/detect-non-literal-fs-filename": "warn",
"security/detect-possible-timing-attacks": "warn"
}
}
Estos plugins corren como parte de tu paso normal de linting, asi que detectan problemas antes de que el codigo llegue al PR.
Escaneo de dependencias
Tu codigo de aplicacion es probablemente el 10% del codigo que realmente se ejecuta. El otro 90% viene de las dependencias. Y esas dependencias tienen sus propias dependencias (dependencias transitivas). Una vulnerabilidad en una dependencia transitiva profundamente anidada puede ser tan peligrosa como una en tu propio codigo.
Esto no es teorico. La vulnerabilidad Log4Shell (CVE-2021-44228) estaba en una libreria de logging que era una dependencia transitiva en miles de aplicaciones Java. La mayoria de los equipos ni sabian que la estaban usando hasta que salio el CVE.
npm audit es el punto de partida mas simple para proyectos Node.js:
# Chequear vulnerabilidades conocidas
npm audit
# Arreglar automaticamente donde sea posible
npm audit fix
# Fallar en CI si hay vulnerabilidades altas o criticas
npm audit --audit-level=high
Agrega esto a tu pipeline de CI:
dependency-scan:
name: Dependency Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: npm ci
- name: Run npm audit
run: npm audit --audit-level=high
Dependabot viene integrado en GitHub y crea pull requests automaticamente cuando hay parches de vulnerabilidades disponibles. Activalo agregando un archivo de configuracion:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
reviewers:
- "your-team"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
Fijate que estamos escaneando tres ecosistemas: paquetes npm, imagenes base de Docker y versiones de GitHub Actions. Cada uno es una superficie de ataque potencial.
Snyk es otra opcion popular que provee analisis mas profundo que npm audit, incluyendo sugerencias de arreglo y priorizacion basada en explotabilidad. Tiene un tier gratuito para proyectos open source.
El habito clave aca es: trata las actualizaciones de dependencias como mantenimiento de seguridad, no como tareas opcionales. Cuando Dependabot abre un PR, revisalo y mergealo rapido. Las dependencias desactualizadas son uno de los vectores de ataque mas comunes.
Escaneo de imagenes de contenedores con Trivy
Tus imagenes Docker contienen un sistema operativo entero mas tu aplicacion y sus dependencias. Cada paquete en ese SO es una vulnerabilidad potencial. Trivy es un scanner open-source que chequea tus imagenes de contenedores (y tus Dockerfiles, y tus manifiestos de Kubernetes) buscando vulnerabilidades conocidas.
Primero, escanea tu Dockerfile buscando misconfiguraciones:
# Instalar Trivy
brew install trivy # macOS
# o: sudo apt-get install trivy # Ubuntu
# Escanear un Dockerfile buscando misconfiguraciones
trivy config Dockerfile
# Escanear una imagen construida buscando vulnerabilidades
trivy image myapp:latest
# Mostrar solo vulnerabilidades altas y criticas
trivy image --severity HIGH,CRITICAL myapp:latest
# Fallar si se encuentran vulnerabilidades criticas (util para CI)
trivy image --severity CRITICAL --exit-code 1 myapp:latest
Problemas comunes que Trivy detecta en Dockerfiles:
- Corriendo como root: Tu contenedor deberia usar un usuario no-root. Agrega
USER nonroota tu Dockerfile.- Usando el tag latest: Siempre fija tu imagen base a una version o digest especifico.
- Sin health checks: Agrega una instruccion
HEALTHCHECKpara que los orquestadores sepan cuando tu app no esta sana.- Datos sensibles en las capas: Nunca hagas
COPYde secretos a tu imagen. Usa build args o monta secretos en runtime.
Aca hay un job de CI que escanea tu imagen despues de construirla:
image-scan:
name: Container Image Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Run Trivy scan
uses: aquasecurity/[email protected]
with:
image-ref: myapp:${{ github.sha }}
format: table
exit-code: 1
severity: HIGH,CRITICAL
ignore-unfixed: true
El flag ignore-unfixed: true omite vulnerabilidades que no tienen un fix disponible todavia. Esto
evita que tu pipeline se bloquee por problemas que no podes resolver. Igual deberias trackear las
vulnerabilidades sin fix, pero no deberian romper tu build.
OIDC para autenticacion de CI/CD
Si tus workflows de GitHub Actions deployean a AWS (o cualquier proveedor cloud), necesitas credenciales. La forma vieja era guardar access keys de larga duracion como GitHub Secrets. El problema es que esas keys nunca expiran, existen en multiples lugares, y si se filtran, un atacante tiene acceso persistente a tu cuenta de AWS.
OIDC (OpenID Connect) resuelve esto permitiendo que GitHub Actions solicite credenciales de corta duracion directamente de AWS. No hay keys de larga duracion guardadas en ningun lado. Las credenciales duran lo que dura la ejecucion del workflow y despues expiran.
Asi se configura:
Paso 1: Crear un proveedor de identidad OIDC en AWS
# Crear el proveedor OIDC (configuracion unica)
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--thumbprint-list "6938fd4d98bab03faadb97b34396831e3780aea1" \
--client-id-list "sts.amazonaws.com"
Paso 2: Crear un rol IAM con una politica de confianza
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::ACCOUNT_ID::oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:ref:refs/heads/main"
}
}
}
]
}
El bloque Condition es importante. Restringe que repositorio y rama pueden asumir este rol. Sin
el, cualquier repositorio de GitHub podria usar tus credenciales de AWS.
Paso 3: Usar OIDC en tu workflow
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write # Requerido para OIDC
contents: read
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::ACCOUNT_ID::role/github-actions-deploy
aws-region: us-east-1
- name: Deploy
run: |
# Estas credenciales son de corta duracion y con scope a esta ejecucion
aws sts get-caller-identity
# ... tus comandos de deployment
La linea permissions.id-token: write es lo que habilita OIDC. Sin ella, el workflow no puede
solicitar un token del proveedor OIDC de GitHub.
Este patron funciona con AWS, GCP, Azure y cualquier proveedor cloud que soporte OIDC. Si tu proveedor lo soporta, no hay razon para usar keys de larga duracion.
Conceptos basicos de RBAC en Kubernetes
RBAC (Role-Based Access Control) controla quien puede hacer que en tu cluster de Kubernetes. El principio es simple: cada usuario, servicio y automatizacion deberia tener los permisos minimos que necesita para hacer su trabajo, y nada mas.
RBAC tiene cuatro recursos clave:
- Role: Define un conjunto de permisos dentro de un namespace. Por ejemplo, “puede leer pods y services en el namespace staging.”
- ClusterRole: Igual que Role pero aplica en todo el cluster. Usalo para recursos cluster-wide como nodos o namespaces.
- RoleBinding: Conecta un Role a un usuario, grupo o ServiceAccount dentro de un namespace.
- ClusterRoleBinding: Conecta un ClusterRole a un sujeto en todo el cluster.
Aca hay un ejemplo basico que le da a un ServiceAccount de CI/CD permisos para manejar deployments en un namespace especifico:
# Crear un ServiceAccount para tu pipeline CI/CD
apiVersion: v1
kind: ServiceAccount
metadata:
name: ci-deployer
namespace: production
---
# Definir que puede hacer
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: deployer-role
namespace: production
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "update", "patch"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
---
# Vincular el role al ServiceAccount
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ci-deployer-binding
namespace: production
subjects:
- kind: ServiceAccount
name: ci-deployer
namespace: production
roleRef:
kind: Role
name: deployer-role
apiGroup: rbac.authorization.k8s.io
Fijate que el Role solo otorga get, list, update y patch sobre deployments. No otorga
create ni delete. Tampoco otorga acceso a secrets, configmaps ni ningun otro recurso. Este es
el principio de minimo privilegio en accion.
Errores comunes a evitar:
- Usar cluster-admin para todo: El ClusterRole
cluster-adminda acceso total a todo. Nunca lo vincules a service accounts usados por aplicaciones o pipelines de CI.- Usar ServiceAccounts default: Cada namespace tiene un ServiceAccount
default. Si no creas especificos, todos tus pods comparten la misma identidad. Crea ServiceAccounts dedicados para cada aplicacion.- No auditar RBAC: Ejecuta
kubectl auth can-i --list --as=system:serviceaccount:production:ci-deployerpara verificar que puede hacer realmente un ServiceAccount.
Para una exploracion mucho mas profunda de RBAC, incluyendo como funciona a nivel de API HTTP con llamadas curl crudas, mira el RBAC Deep Dive.
Network Policies
Por defecto, cada pod en un cluster de Kubernetes puede hablar con cada otro pod. Esto es conveniente para desarrollo pero terrible para seguridad. Si un atacante compromete un pod, puede alcanzar todos los demas servicios en el cluster.
Las Network Policies te permiten controlar que pods se pueden comunicar con cuales otros. Pensalas como reglas de firewall para la red interna de tu cluster.
Paso 1: Empezar con una politica de deny por defecto
Esto bloquea todo el trafico ingress hacia los pods en el namespace. Nada puede hablar con nada a menos que lo permitas explicitamente.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: production
spec:
podSelector: {} # Aplica a todos los pods en el namespace
policyTypes:
- Ingress
Paso 2: Permitir rutas de trafico especificas
Ahora abris agujeros para el trafico que necesita fluir. Por ejemplo, dejar que la API reciba trafico del ingress controller:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-ingress-to-api
namespace: production
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: ingress-system
- podSelector:
matchLabels:
app: ingress-controller
ports:
- port: 3000
protocol: TCP
Y dejar que la API se comunique con la base de datos:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-api-to-db
namespace: production
spec:
podSelector:
matchLabels:
app: postgres
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: api
ports:
- port: 5432
protocol: TCP
El patron es siempre el mismo: negar todo por defecto, y despues permitir solo las rutas especificas que tu aplicacion necesita. Documenta estas rutas. Si no podes explicar por que existe una network policy, probablemente no deberia existir.
Nota importante: las Network Policies requieren un plugin CNI que las soporte. Si estas usando EKS, el VPC CNI por defecto no aplica Network Policies. Necesitas habilitar la funcionalidad de Network Policy o usar un CNI como Calico. Revisa la documentacion del CNI de tu cluster.
Pod Security Standards
Los Pod Security Standards (PSS) definen tres perfiles que controlan que pueden hacer los pods a nivel de seguridad:
- Privileged: Sin restricciones. Usalo solo para pods a nivel de sistema como plugins CNI o drivers de almacenamiento que genuinamente necesitan acceso al host.
- Baseline: Previene las configuraciones mas peligrosas como correr como privilegiado, usar networking del host, o montar el filesystem del host. Este es un default razonable para la mayoria de los workloads.
- Restricted: El perfil mas estricto. Requiere correr como non-root, eliminar todas las capabilities de Linux, configurar un root filesystem de solo lectura, y mas. Esto es lo que las aplicaciones de produccion deberian apuntar.
La forma mas simple de aplicar estos es con labels de namespace. Kubernetes tiene un admission controller integrado llamado Pod Security Admission que lee estos labels y aplica el perfil correspondiente.
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
# Enforce: rechazar pods que violen el perfil restricted
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latest
# Warn: loguear un warning para pods que violen restricted
# (util durante la migracion para ver que se romperia)
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: latest
# Audit: agregar una anotacion de auditoria para violaciones de baseline
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/audit-version: latest
Cuando aplicas el label enforce: restricted, Kubernetes va a rechazar cualquier pod que no cumpla
con el perfil restricted. Por ejemplo, si tu pod spec no incluye runAsNonRoot: true, el pod va a
ser rechazado en el momento de la admision.
Aca esta como se ve un pod spec que cumple con el perfil restricted:
apiVersion: v1
kind: Pod
metadata:
name: secure-app
namespace: production
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: myapp:1.0.0@sha256:abc123...
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
limits:
memory: "128Mi"
cpu: "500m"
requests:
memory: "64Mi"
cpu: "250m"
Si estas migrando workloads existentes, empeza con modo warn para ver que fallaria, arregla las
violaciones y despues cambia a enforce. No saltes directo a enforce en un namespace de produccion
a menos que hayas testeado todos los workloads.
Higiene de secretos
La gestion de secretos se cubre en profundidad en el articulo de Secrets y Configuracion de esta serie. Aca nos vamos a enfocar en las practicas de higiene que previenen que los secretos se filtren en primer lugar.
Nunca loguees secretos. Esto suena obvio, pero pasa todo el tiempo. Un statement de debug log imprime el objeto request entero, que incluye el header Authorization. Un script de arranque hace echo de las variables de entorno para verificar la configuracion. Un manejador de errores hace dump del contexto completo, incluyendo strings de conexion a la base de datos. Todo esto termina en tu sistema de logging, que generalmente es accesible para mucha mas gente de la que deberia tener acceso a tus secretos.
Reglas practicas:
- Redacta campos sensibles en el logging: Configura tu libreria de logging para redactar campos como
password,token,secret,authorizationycookie. La mayoria de las librerias de logging soportan esto.- Nunca hagas echo de secretos en logs de CI: Si tu pipeline de CI necesita un secreto, usa variables enmascaradas. GitHub Actions enmascara secretos automaticamente, pero solo si los referenciascon
${{ secrets.NAME }}. Si copias el valor a una variable regular y haces echo, el enmascaramiento no aplica.- Rota secretos regularmente: Establece un calendario de rotacion. Como minimo, rota cada 90 dias. Rota inmediatamente si alguien deja el equipo o si sospechas una filtracion.
- Audita quien tiene acceso: Periodicamente revisa quien puede leer tus secretos. En Kubernetes, chequea que ServiceAccounts tienen
getolistsobre secrets. En GitHub, revisa quien tiene acceso a los secrets del repositorio.- Usa tokens de corta duracion: Siempre que sea posible, usa tokens que expiren. Tokens OIDC, JWTs con expiracion corta, credenciales temporales de AWS. Los tokens de larga duracion son un pasivo.
Una forma rapida de chequear secretos hardcodeados en tu codebase antes de que se commiteen:
# Instalar gitleaks
brew install gitleaks # macOS
# Escanear el repo actual
gitleaks detect --source . --verbose
# Agregar como pre-commit hook
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
Seguridad de la cadena de suministro
Los ataques a la cadena de suministro apuntan a las herramientas y dependencias que usas en vez de a tu codigo directamente. El ataque de SolarWinds, la brecha de Codecov y el secuestro de ua-parser-js en npm son todos ejemplos. No podes eliminar el riesgo de cadena de suministro completamente, pero podes reducir tu exposicion significativamente.
Pinear versiones de actions por SHA, no por tag
Los tags de GitHub Actions son mutables. Un actor malicioso que comprometa el repositorio de una
action popular puede actualizar el tag v4 para apuntar a codigo malicioso, y cada workflow usando
actions/checkout@v4 lo ejecutaria. Pinear por SHA hace tu workflow reproducible y resistente a
manipulacion:
# En vez de esto (el tag se puede mover):
- uses: actions/checkout@v4
# Usa esto (el SHA es inmutable):
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
Si, es menos legible. Agrega un comentario con el numero de version. La seguridad vale el trade-off.
Verificar imagenes base
Usa imagenes oficiales de registries confiables. Pinealas por digest, no solo por tag:
# En vez de esto (el tag se puede sobreescribir):
FROM node:20-alpine
# Usa esto (el digest es content-addressable e inmutable):
FROM node:20-alpine@sha256:abcdef1234567890...
Podes encontrar el digest en Docker Hub o ejecutando docker inspect --format='{{index .RepoDigests 0}}' node:20-alpine.
SBOM (Software Bill of Materials)
Un SBOM es un inventario de cada componente en tu aplicacion. Cuando sale un nuevo CVE, un SBOM te
dice inmediatamente si estas afectado. No tenes que ir a escarbar en node_modules o capas de
Docker.
Trivy puede generar SBOMs:
# Generar un SBOM en formato SPDX
trivy image --format spdx-json --output sbom.json myapp:latest
# Generar en formato CycloneDX
trivy image --format cyclonedx --output sbom.xml myapp:latest
Guarda tu SBOM como un artefacto de build en tu pipeline de CI para poder referenciarlo despues cuando se divulguen nuevas vulnerabilidades.
Para un tratamiento mas completo de seguridad de la cadena de suministro incluyendo firma de imagenes con Cosign y politicas de Kyverno, mira el articulo de SRE Security as Code.
La checklist practica de seguridad
Aca hay una lista top-10 de cosas que todo proyecto deberia implementar. Estan ordenadas aproximadamente por impacto y esfuerzo, asi que empeza desde arriba y avanza hacia abajo.
- 1. Habilitar Dependabot o equivalente: Activa actualizaciones automaticas de dependencias para todos tus ecosistemas (npm, Docker, GitHub Actions). Esto toma cinco minutos y detecta la mayoria de las vulnerabilidades conocidas automaticamente.
- 2. Agregar escaneo de secretos a tu repo: Habilita GitHub secret scanning o agrega gitleaks como pre-commit hook. Esto previene filtraciones accidentales de credenciales, que son la causa numero uno de brechas en equipos chicos.
- 3. Escanear imagenes de contenedores en CI: Agrega Trivy o un scanner similar a tu pipeline de build. Falla el build en vulnerabilidades criticas. Esto detecta vulnerabilidades a nivel de SO que tus scanners a nivel de lenguaje no captan.
- 4. Usar OIDC en vez de keys de larga duracion: Si tu CI deployea a un proveedor cloud, cambia a autenticacion OIDC. Elimina cualquier access key de larga duracion de tus GitHub Secrets.
- 5. Correr contenedores como non-root: Actualiza tus Dockerfiles para usar un usuario non-root. Aplica Pod Security Standards a nivel de namespace para forzar esto en todo el cluster.
- 6. Implementar network policies: Empeza con default-deny y permite explicitamente el trafico que tu aplicacion necesita. Esto limita el radio de explosion si un pod se ve comprometido.
- 7. Crear roles RBAC dedicados: Deja de usar cluster-admin y ServiceAccounts default. Crea Roles especificos con permisos minimos para cada workload y pipeline de CI.
- 8. Agregar SAST a tu pipeline de CI: Agrega Semgrep o herramientas SAST equivalentes. Incluso los paquetes de reglas por defecto detectan una cantidad sorprendente de problemas reales.
- 9. Pinear tus dependencias: Pinea versiones de actions por SHA, pinea imagenes base por digest, y usa lock files para package managers. Esto protege contra ataques a la cadena de suministro.
- 10. Rotar secretos con un calendario: Pone recordatorios en el calendario para rotar API keys, passwords de base de datos y tokens de service accounts cada 90 dias. Automatiza la rotacion donde sea posible.
No necesitas hacer las diez en un sprint. Empeza con los items 1 a 4. Son quick wins con alto impacto. Despues avanza con el resto a medida que madure tu postura de seguridad.
Juntando todo en CI
Aca hay un workflow de seguridad completo que combina varios de los chequeos que discutimos. Podes agregar esto junto a tu pipeline de CI existente:
# .github/workflows/security.yml
name: Security
on:
pull_request:
branches: [main]
push:
branches: [main]
schedule:
# Correr semanalmente incluso sin cambios de codigo para detectar nuevos CVEs
- cron: "0 8 * * 1"
jobs:
sast:
name: SAST
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Semgrep
uses: semgrep/semgrep-action@v1
with:
config: >-
p/security-audit
p/secrets
p/owasp-top-ten
dependencies:
name: Dependency Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Install dependencies
run: npm ci
- name: npm audit
run: npm audit --audit-level=high
image-scan:
name: Image Scan
runs-on: ubuntu-latest
needs: [sast, dependencies] # Solo escanear si los chequeos de codigo pasan
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Trivy scan
uses: aquasecurity/[email protected]
with:
image-ref: myapp:${{ github.sha }}
format: table
exit-code: 1
severity: HIGH,CRITICAL
ignore-unfixed: true
- name: Generate SBOM
if: github.ref == 'refs/heads/main'
run: |
trivy image --format spdx-json \
--output sbom.json myapp:${{ github.sha }}
- name: Upload SBOM
if: github.ref == 'refs/heads/main'
uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.json
Algunas cosas para notar:
- Ejecuciones programadas: El trigger
cronejecuta el escaneo semanalmente incluso si no hay cambios de codigo. Nuevos CVEs se publican constantemente, y una dependencia que estaba limpia la semana pasada puede tener una vulnerabilidad critica hoy.- Actions pineadas por SHA: Practicamos lo que predicamos. La action de checkout esta pineada a un commit especifico.
- SBOM como artefacto: En builds de la rama main, generamos y guardamos un SBOM para tener un registro de exactamente que fue a cada release.
- Fallar rapido: El escaneo de imagen solo corre si los chequeos SAST y de dependencias pasan. No tiene sentido escanear una imagen si el codigo en si tiene problemas.
Notas finales
La seguridad no es un proyecto con fecha de finalizacion. Es una practica, como testing o code review. El objetivo no es hacer tu sistema impenetrable (nada lo es), sino hacerlo lo suficientemente dificil para que los atacantes se muevan a objetivos mas faciles, y limitar el dano cuando algo logra pasar.
En este articulo cubrimos la mentalidad shift-left de seguridad, SAST con Semgrep y plugins de ESLint, escaneo de dependencias con npm audit y Dependabot, escaneo de imagenes de contenedores con Trivy, autenticacion OIDC para CI/CD, conceptos basicos de RBAC en Kubernetes, network policies con default deny, Pod Security Standards y enforcement por namespace, practicas de higiene de secretos, seguridad de la cadena de suministro con versiones pineadas y SBOMs, y una checklist practica de seguridad top-10.
Cada tema aca fue cubierto a nivel de checklist. Si queres ir mas profundo, el articulo de SRE Security as Code cubre OPA Gatekeeper, seguridad en runtime con Falco, firma de imagenes con Cosign y frameworks de policy-as-code. El articulo de RBAC Deep Dive recorre RBAC a nivel de API de Kubernetes con llamadas HTTP crudas.
Empeza con la checklist. Elegí los tres o cuatro items que tu proyecto le faltan e implementalos esta semana. La seguridad es una de esas cosas donde hacer algo es infinitamente mejor que no hacer nada.
En el proximo articulo vamos a cubrir disaster recovery y estrategias de backup, la ultima capa de proteccion cuando todo lo demas falla.
Espero que te haya resultado util y que hayas disfrutado la lectura, hasta la proxima!
Errata
Si encontras algun error o tenes alguna sugerencia, por favor mandame un mensaje para que se corrija.
Tambien, podes ver el codigo fuente y los cambios en los fuentes aca
$ Comentarios
Online: 0Por favor inicie sesión para poder escribir comentarios.