diff --git a/butane/oswald_runner.yaml.tftpl b/butane/oswald_runner.yaml.tftpl index 0b36918..4485d3f 100644 --- a/butane/oswald_runner.yaml.tftpl +++ b/butane/oswald_runner.yaml.tftpl @@ -42,6 +42,59 @@ systemd: RestartSec=5s [Install] WantedBy=multi-user.target + # n8n workflow automation, served publicly at n8n.atlashorizon.net. + # + # Prerequisites, all of which must exist before the VM is rebuilt: + # - an "n8n" Postgres database and role on argo.lan.atlashorizon.net + # - N8N_DB_PASSWORD and N8N_ENCRYPTION_KEY in Infisical (see + # secrets.tmpl); generate the key with `openssl rand -hex 32` + # - n8n.atlashorizon.net terminating TLS and proxying to this host's + # :5678 (not managed here) + # + # Workflows, credentials and execution history all live in Postgres, so + # a taint loses nothing. The encryption key is what makes that true: + # without a fixed one n8n generates its own under /home/node/.n8n, and + # the credentials in Postgres become undecryptable the moment the VM + # disk goes. Never rotate it without re-entering every credential. + - name: n8n.service + enabled: true + contents: | + [Unit] + Description=n8n workflow automation + After=docker.service + Requires=docker.service + [Service] + TimeoutStartSec=0 + ExecStartPre=/usr/bin/docker pull docker.n8n.io/n8nio/n8n:2.40.5 + ExecStartPre=-/usr/bin/docker rm --force n8n + # /var/lib/n8n holds only what is left in /home/node/.n8n once the + # database and key are external: installed community nodes and + # n8n's own config file. Losing it to a taint means reinstalling + # community nodes, nothing more. + # + # N8N_PROXY_HOPS=1: requests arrive through the reverse proxy, so + # n8n takes the client IP from X-Forwarded-For (rate limiting and + # audit logs would otherwise see only the proxy). + ExecStart=/usr/bin/docker run --name n8n \ + -e DB_TYPE=postgresdb \ + -e DB_POSTGRESDB_HOST=argo.lan.atlashorizon.net \ + -e DB_POSTGRESDB_PORT=5432 \ + -e DB_POSTGRESDB_DATABASE=n8n \ + -e DB_POSTGRESDB_USER=n8n \ + -e DB_POSTGRESDB_PASSWORD=${n8n_db_password} \ + -e N8N_ENCRYPTION_KEY=${n8n_encryption_key} \ + -e N8N_HOST=n8n.atlashorizon.net \ + -e N8N_PROTOCOL=https \ + -e WEBHOOK_URL=https://n8n.atlashorizon.net/ \ + -e N8N_PROXY_HOPS=1 \ + -v /var/lib/n8n:/home/node/.n8n \ + -p 5678:5678 \ + docker.n8n.io/n8nio/n8n:2.40.5 + ExecStop=/usr/bin/docker stop n8n + Restart=always + RestartSec=5s + [Install] + WantedBy=multi-user.target - name: dozzle-agent.service enabled: true contents: | @@ -66,3 +119,11 @@ storage: directories: - path: /var/lib/oswald_runner mode: 0755 + # The n8n image runs as the "node" user (uid 1000), which needs to own + # its data directory. + - path: /var/lib/n8n + mode: 0700 + user: + id: 1000 + group: + id: 1000 diff --git a/secrets.tmpl b/secrets.tmpl index b6a6b00..1b3adb9 100644 --- a/secrets.tmpl +++ b/secrets.tmpl @@ -24,6 +24,8 @@ {{- if eq $secret.Key "OPENWEBUI_OIDC_PROVIDER_URL" }}"openwebui_oidc_provider_url": "{{$secret.Value}}",{{ end }} {{- if eq $secret.Key "OPENWEBUI_TERMINAL_API_KEY" }}"openwebui_terminal_api_key": "{{$secret.Value}}",{{ end }} {{- if eq $secret.Key "OPENWEBUI_ADMIN_API_KEY" }}"openwebui_admin_api_key": "{{$secret.Value}}",{{ end }} + {{- if eq $secret.Key "N8N_DB_PASSWORD" }}"n8n_db_password": "{{$secret.Value}}",{{ end }} + {{- if eq $secret.Key "N8N_ENCRYPTION_KEY" }}"n8n_encryption_key": "{{$secret.Value}}",{{ end }} {{- if eq $secret.Key "DOZZLE_OIDC_CLIENT_ID" }}"dozzle_oidc_client_id": "{{$secret.Value}}",{{ end }} {{- if eq $secret.Key "DOZZLE_OIDC_CLIENT_SECRET" }}"dozzle_oidc_client_secret": "{{$secret.Value}}",{{ end }} {{- end }} diff --git a/terraform.tfvars b/terraform.tfvars index 39dc9eb..5c1c609 100644 --- a/terraform.tfvars +++ b/terraform.tfvars @@ -209,7 +209,8 @@ nodes = { qemu_guest_agent = false # Rendered with templatefile(): ${gitea_runner_token} comes from # Infisical (GITEA_RUNNER_TOKEN in secrets.tmpl), so the token is - # never committed. + # never committed. Also runs n8n on :5678 (N8N_* secrets, Postgres + # on argo); see the prerequisites in the Butane file. butane_file = "butane/oswald_runner.yaml.tftpl" } "ai" = {