Files
k8s-manifests/n8n/deployment-n8n.yaml
T
chemavxandClaude Opus 5 50f090bbea Cerrar el auto-reinicio: fail-closed, permisos por nombre y fallo visible
Tres agujeros que encontro una review adversarial de Codex sobre el trabajo
de ayer. Los tres verificados contra el fichero antes de tocar nada.

1. El webhook publico fallaba en ABIERTO. Con `optional: true` y un
   `if (secreto && ...)`, la ausencia del secreto equivalia a autorizacion:
   cualquier POST con heartbeat.status=0 y un monitor.name del mapa reiniciaba
   n8n, Gitea, ArgoCD, Vaultwarden o los blogs. El argumento para dejarlo asi
   era "que un despiste no te deje sin avisos", y era falso: el aviso de caida
   lo manda Kuma por su notificacion 1, directa, no por este webhook.
   Ahora el secreto es obligatorio (sin `optional`) y la puerta rechaza si
   falta. Secreto ausente da error ruidoso; cabecera que no casa, vacio.

2. El ClusterRole permitia patch sobre CUALQUIER deployment de 10 namespaces,
   y la SA va montada en el pod entero de n8n: cualquier workflow con un nodo
   Code heredaba eso. Ahora son Roles por namespace con resourceNames sobre
   los 12 workloads exactos del SERVICE_MAP.

3. El flujo daba exito aunque el PATCH fallara. `restartOk` se calculaba y se
   tiraba: `K8s API Check Status` construia un objeto nuevo sin el, y el IF
   final solo miraba `statusOk`. Un 403 sobre un servicio ya sano mandaba
   "reiniciado correctamente". Ahora hay rama de fallo tras el PATCH y el
   exito exige observedGeneration >= la generation que devolvio el PATCH.
   Se compara con >= porque selfHeal de ArgoCD revierte la anotacion y vuelve
   a subir la generation; con == daria un fallo falso.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 08:02:07 +00:00

145 lines
6.5 KiB
YAML

apiVersion: apps/v1
kind: Deployment
metadata:
name: n8n
namespace: n8n
annotations:
# El operator de Infisical redespliega este Deployment cuando cambia
# n8n-secret-infisical (p.ej. rotación de GETXAPI_TOKEN) — sin kubectl manual.
secrets.infisical.com/auto-reload: "true"
spec:
replicas: 1
selector:
matchLabels:
app: n8n
# Single-replica n8n with a 485MB SQLite DB (+ active WAL) on a RWO local-path
# PVC: Recreate fully terminates the old pod (releasing the PVC and closing the
# SQLite-WAL) before the new pod starts — avoids two writers racing one DB file
# (RollingUpdate would surge a 2nd pod first).
strategy:
type: Recreate
template:
metadata:
labels:
app: n8n
spec:
# El workflow del auto-reinicio lee el token proyectado de ESTA SA para
# hablar con el API de K8s (ver rbac-restarter.yaml). Hasta el 2026-07-30
# heredaba la "default", que no tiene ningún permiso: 403 en cada intento.
serviceAccountName: n8n-restarter
securityContext:
fsGroup: 1000
runAsUser: 1000
containers:
- name: n8n
image: git.chemavx.xyz/chemavx/n8n:01d60680
imagePullPolicy: IfNotPresent
ports:
- containerPort: 5678
env:
- name: N8N_ENCRYPTION_KEY
valueFrom:
secretKeyRef:
name: n8n-secret-infisical
key: encryption-key
# Token getxapi (X autopost) — rotado 2026-07-09; el workflow lo lee
# con la expresión {{ $env.GETXAPI_TOKEN }}, nunca inline en el nodo.
- name: GETXAPI_TOKEN
valueFrom:
secretKeyRef:
name: n8n-secret-infisical
key: GETXAPI_TOKEN
# Token del bot @chemavx_bot (notificaciones) — rotado 2026-07-09.
# Lo consume el workflow "Health Check General" vía process.env,
# ya no hardcodeado. Fuente: Infisical homelab/prod/telegram.
- name: TELEGRAM_BOT_TOKEN
valueFrom:
secretKeyRef:
name: telegram-notify-infisical
key: TELEGRAM_BOT_TOKEN
- name: TELEGRAM_CHAT_ID
valueFrom:
secretKeyRef:
name: telegram-notify-infisical
key: TELEGRAM_CHAT_ID
# Secreto compartido con la notificación "n8n Auto-Restart" de Uptime
# Kuma, que lo manda en la cabecera X-Kuma-Token (2026-07-30). El
# webhook /webhook/uptime-kuma-restart es público y desde hoy sí
# reinicia servicios de verdad, así que no puede quedar abierto.
# ⚠️ FUERA DE GIT a propósito (secret n8n-kuma-webhook, creado con
# kubectl).
#
# SIN "optional: true" desde el 2026-07-31: si el secreto falta, el
# pod NO arranca. Antes arrancaba y el workflow se saltaba la
# comprobación, o sea que la ausencia de credencial equivalía a
# autorización en un endpoint expuesto a internet. El argumento para
# dejarlo abierto era "que un despiste no te deje sin avisos", y era
# falso: el aviso de caída lo manda Kuma por su notificación 1
# (Telegram, directa). Fallar en cerrado no cuesta ni un aviso —
# solo el reinicio automático. Un pod que no arranca se ve; un
# webhook abierto, no.
- name: KUMA_WEBHOOK_TOKEN
valueFrom:
secretKeyRef:
name: n8n-kuma-webhook
key: KUMA_WEBHOOK_TOKEN
# n8n ≥1.x bloquea $env en expresiones por defecto; sin esto el nodo
# HTTP del autopost no puede leer GETXAPI_TOKEN. Instancia single-user.
- name: N8N_BLOCK_ENV_ACCESS_IN_NODE
value: "false"
- name: N8N_HOST
value: n8n.chemavx.xyz
- name: N8N_PORT
value: "5678"
- name: N8N_PROTOCOL
value: https
- name: WEBHOOK_URL
value: https://n8n.chemavx.xyz/
- name: N8N_USER_FOLDER
value: /home/node/.n8n
- name: NODE_FUNCTION_ALLOW_EXTERNAL
value: "*"
# ALLOW_EXTERNAL es para paquetes npm; los módulos INTERNOS de Node
# necesitan esta otra, y sin ella el task runner responde
# "Module 'https' is disallowed" (2026-07-30). Tenía tres workflows
# activos rotos sin que saltara nada: el auto-reinicio que dispara
# Uptime Kuma (último éxito 23-jul 07:51, se rompió al reiniciarse el
# pod esa tarde), "TLS Certs → Alerta Telegram" (último éxito 13-abr)
# y "Resumen Diario — Métricas K8s" (30 fallos, 0 éxitos).
# Lista explícita y no "*": es exactamente lo que piden los nodos Code
# de esos workflows, y si algún día hace falta otro módulo, que falle
# a la vista en vez de conceder todo por defecto.
- name: NODE_FUNCTION_ALLOW_BUILTIN
value: "fs,http,https"
- name: GENERIC_TIMEZONE
value: Europe/Madrid
- name: DB_TYPE
value: sqlite
- name: DB_SQLITE_DATABASE
value: /home/node/.n8n/database.sqlite
# No guardar el payload de las ejecuciones que salen BIEN (2026-07-30).
# La poda por edad ya venía activa de serie (336 h), así que la BD no
# se desbocaba; el problema era otro: de los 322 MB de execution_data,
# 246 MB eran las 3.436 tiradas VERDES de "Real Madrid RSS → Twitter",
# que corre cada 5 min y guarda 73 KB por vuelta. Los fallos, que son
# lo que se depura, ocupaban 2 MB. Y /data/n8n va entero en la lista
# CROWN del backup: eso viajaba a MEGA a diario y a B2 en las dos
# últimas copias — justo lo que reventó la cuota el 2026-07-25.
# Los errores se siguen guardando enteros (SAVE_ON_ERROR=all, defecto).
- name: EXECUTIONS_DATA_SAVE_ON_SUCCESS
value: none
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 1Gi
volumeMounts:
- name: n8n-data
mountPath: /home/node/.n8n
volumes:
- name: n8n-data
persistentVolumeClaim:
claimName: n8n-pvc