Files
oswald-infra/butane/oswald_runner.yaml.tftpl
T

385 lines
17 KiB
Plaintext

variant: flatcar
version: 1.0.0
systemd:
units:
# Gitea Actions runner. Registers once on first start and keeps its
# identity (the .runner file) in /var/lib/oswald_runner on the VM's own
# root disk -- not ephemeral, so it does not self-delete after each job
# (that caused stale offline entries whenever the container was
# interrupted before finishing a job; see flatcar-todo.md). Reusing the
# same registration also means the token never needs rotating.
#
# /var/lib/oswald_runner survives container restarts and VM reboots (it's
# on disk, not in the container), but not a VM rebuild (taint): that
# recreates the disk from the image, so the runner re-registers fresh
# next boot. Old entries from before this change, or from any future
# rebuild, still need manual cleanup in Gitea's runner admin page.
#
# GITEA_RUNNER_REGISTRATION_TOKEN is templated in from Infisical
# (GITEA_RUNNER_TOKEN in secrets.tmpl) by main.tf, not stored here. It
# still ends up in plaintext in the rendered Ignition snippet on the
# node's storage -- see the caveat in flatcar-todo.md.
- name: oswald_runner.service
enabled: true
contents: |
[Unit]
Description=Gitea Actions runner
After=docker.service versions-fetch.service
Wants=versions-fetch.service
Requires=docker.service
[Service]
# Image tag comes from versions.env; see butane/versions-sync.yaml.
EnvironmentFile=/etc/versions.env
TimeoutStartSec=0
ExecStartPre=-/usr/bin/docker rm --force oswald_runner
ExecStart=/usr/bin/docker run --name oswald_runner \
-e GITEA_INSTANCE_URL=https://git.atlashorizon.net \
-e GITEA_RUNNER_REGISTRATION_TOKEN=${gitea_runner_token} \
-e GITEA_RUNNER_NAME=oswald_runner \
-v /var/lib/oswald_runner:/data \
-v /var/run/docker.sock:/var/run/docker.sock \
$${GITEA_RUNNER_IMAGE}
ExecStop=/usr/bin/docker stop oswald_runner
Restart=always
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 versions-fetch.service
Wants=versions-fetch.service
Requires=docker.service
[Service]
# Image tag comes from versions.env; see butane/versions-sync.yaml.
EnvironmentFile=/etc/versions.env
TimeoutStartSec=0
ExecStartPre=/usr/bin/docker pull $${N8N_IMAGE}
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 \
-e N8N_DIAGNOSTICS_ENABLED=false \
-v /var/lib/n8n:/home/node/.n8n \
-p 5678:5678 \
$${N8N_IMAGE}
ExecStop=/usr/bin/docker stop n8n
Restart=always
RestartSec=5s
[Install]
WantedBy=multi-user.target
# CISA KEV RSS feed. Turns the CISA Known Exploited Vulnerabilities
# catalog into an RSS 2.0 feed (https://github.com/xsaveopt/cisa-kev-rss).
#
# Feed: http://192.168.7.60:3000/rss
# Health: http://192.168.7.60:3000/health
#
# Stateless: the container refreshes from CISA every ten minutes and keeps
# nothing on disk. Moved here from its own VM (cisa-kev-rss, 192.168.7.64).
- name: cisa-kev-rss.service
enabled: true
contents: |
[Unit]
Description=CISA KEV RSS feed
After=docker.service versions-fetch.service
Wants=versions-fetch.service
Requires=docker.service
[Service]
# Image tag comes from versions.env; see butane/versions-sync.yaml.
EnvironmentFile=/etc/versions.env
TimeoutStartSec=0
ExecStartPre=/usr/bin/docker pull $${CISA_KEV_RSS_IMAGE}
ExecStartPre=-/usr/bin/docker rm --force cisa-kev-rss
ExecStart=/usr/bin/docker run --rm --name cisa-kev-rss \
-p 3000:3000 \
$${CISA_KEV_RSS_IMAGE}
ExecStop=/usr/bin/docker stop cisa-kev-rss
Restart=always
RestartSec=5s
[Install]
WantedBy=multi-user.target
# Domain Watchdog: RDAP-based domain expiry/change monitoring
# (https://github.com/maelgangloff/domain-watchdog), served at
# domainwatchdog.atlashorizon.net. Three containers on a private docker
# network: the web app on :8083, a messenger worker, and Valkey as the
# job queue and lock store.
#
# Prerequisites, all of which must exist before the VM is rebuilt:
# - a "domainwatchdog" Postgres database and role on
# argo.lan.atlashorizon.net; the schema is migrated on every start
# - an OIDC client in Authelia with redirect URI
# https://domainwatchdog.atlashorizon.net/login/oauth/check, scopes
# openid/email/profile, and token_endpoint_auth_method
# client_secret_post (the app sends the secret in the POST body)
# - DOMAIN_WATCHDOG_DB_PASSWORD, DOMAIN_WATCHDOG_APP_SECRET,
# DOMAIN_WATCHDOG_OIDC_CLIENT_ID and DOMAIN_WATCHDOG_OIDC_CLIENT_SECRET
# in Infisical (see secrets.tmpl). Every Butane file is rendered on
# every plan, so until they exist no plan succeeds. Generate the app
# secret with `openssl rand -hex 32`; keep the DB password URL-safe
# and free of "%" (it is spliced into DATABASE_URL, and systemd reads
# "%" as a specifier)
# - domainwatchdog.atlashorizon.net terminating TLS and proxying to this
# host's :8083 with X-Forwarded-Proto/Host (not managed here)
#
# Users, watchlists and domain history all live in Postgres, so a taint
# loses nothing that matters. What does not survive it:
# - queued RDAP jobs in Valkey; the scheduler re-queues them
# - the JWT signing keys in /var/lib/domain-watchdog/jwt, which the
# entrypoint regenerates when missing, so everyone logs in again.
# They are on disk rather than in the container only so that a
# plain restart does not do the same.
#
# Login is Authelia-only: registration is off, and the first OIDC login
# for an email creates that user, so Authelia's access rules for the
# client decide who gets in.
- name: domain-watchdog-valkey.service
enabled: true
contents: |
[Unit]
Description=Domain Watchdog Valkey
After=docker.service versions-fetch.service
Wants=versions-fetch.service
Requires=docker.service
[Service]
# Image tag comes from versions.env; see butane/versions-sync.yaml.
EnvironmentFile=/etc/versions.env
TimeoutStartSec=0
ExecStartPre=/usr/bin/docker pull $${DOMAIN_WATCHDOG_VALKEY_IMAGE}
ExecStartPre=-/usr/bin/docker rm --force domain-watchdog-valkey
# Every Domain Watchdog unit tries to create the network, so start
# order does not matter; "-" because all but the first will fail.
ExecStartPre=-/usr/bin/docker network create domain-watchdog
# No volume and no port: the queue is disposable and only the other
# two containers reach it, by name, over the domain-watchdog network.
ExecStart=/usr/bin/docker run --rm --name domain-watchdog-valkey \
--network domain-watchdog \
--network-alias valkey \
$${DOMAIN_WATCHDOG_VALKEY_IMAGE}
ExecStop=/usr/bin/docker stop domain-watchdog-valkey
Restart=always
RestartSec=5s
[Install]
WantedBy=multi-user.target
- name: domain-watchdog.service
enabled: true
contents: |
[Unit]
Description=Domain Watchdog web app
After=docker.service domain-watchdog-valkey.service versions-fetch.service
Requires=docker.service
Wants=domain-watchdog-valkey.service versions-fetch.service
[Service]
# Image tag comes from versions.env; see butane/versions-sync.yaml.
EnvironmentFile=/etc/versions.env
TimeoutStartSec=0
# Secrets are unit environment rather than lines in app.env, which
# is world-readable; docker run forwards them by bare name below.
Environment="APP_SECRET=${domain_watchdog_app_secret}"
Environment="DATABASE_URL=postgresql://domainwatchdog:${domain_watchdog_db_password}@argo.lan.atlashorizon.net:5432/domainwatchdog?serverVersion=16&charset=utf8"
Environment="OAUTH_CLIENT_ID=${domain_watchdog_oidc_client_id}"
Environment="OAUTH_CLIENT_SECRET=${domain_watchdog_oidc_client_secret}"
ExecStartPre=/usr/bin/docker pull $${DOMAIN_WATCHDOG_IMAGE}
ExecStartPre=-/usr/bin/docker rm --force domain-watchdog
ExecStartPre=-/usr/bin/docker network create domain-watchdog
# The entrypoint runs DB migrations and generates the JWT keypair
# into /app/config/jwt if it is missing, then starts FrankenPHP on
# :80 (plain HTTP: SERVER_NAME=:80 in app.env, TLS is the proxy's).
ExecStart=/usr/bin/docker run --rm --name domain-watchdog \
--network domain-watchdog \
--env-file /etc/domain-watchdog/app.env \
-e APP_SECRET \
-e DATABASE_URL \
-e OAUTH_CLIENT_ID \
-e OAUTH_CLIENT_SECRET \
-v /var/lib/domain-watchdog/jwt:/app/config/jwt \
-v /etc/domain-watchdog/content:/app/public/content:ro \
-p 8083:80 \
$${DOMAIN_WATCHDOG_IMAGE}
ExecStop=/usr/bin/docker stop domain-watchdog
Restart=always
RestartSec=5s
[Install]
WantedBy=multi-user.target
# Runs the queued RDAP lookups and notifications. Same image, so the
# entrypoint also runs migrations here; ordering after the web app keeps
# the two from racing on a fresh database. It exits every hour
# (--time-limit, as upstream recommends against PHP memory creep) and
# Restart=always brings it straight back.
- name: domain-watchdog-worker.service
enabled: true
contents: |
[Unit]
Description=Domain Watchdog worker
After=docker.service domain-watchdog-valkey.service domain-watchdog.service versions-fetch.service
Requires=docker.service
Wants=domain-watchdog-valkey.service versions-fetch.service
[Service]
# Image tag comes from versions.env; see butane/versions-sync.yaml.
EnvironmentFile=/etc/versions.env
TimeoutStartSec=0
Environment="APP_SECRET=${domain_watchdog_app_secret}"
Environment="DATABASE_URL=postgresql://domainwatchdog:${domain_watchdog_db_password}@argo.lan.atlashorizon.net:5432/domainwatchdog?serverVersion=16&charset=utf8"
ExecStartPre=/usr/bin/docker pull $${DOMAIN_WATCHDOG_IMAGE}
ExecStartPre=-/usr/bin/docker rm --force domain-watchdog-worker
ExecStartPre=-/usr/bin/docker network create domain-watchdog
# --no-healthcheck: the image's HEALTHCHECK probes FrankenPHP's
# metrics on :2019, which only the web app runs, so the worker would
# always show unhealthy (upstream's compose disables it the same way).
ExecStart=/usr/bin/docker run --rm --name domain-watchdog-worker \
--no-healthcheck \
--network domain-watchdog \
--env-file /etc/domain-watchdog/app.env \
-e APP_SECRET \
-e DATABASE_URL \
-e MESSENGER_CONSUMER_NAME=worker \
$${DOMAIN_WATCHDOG_IMAGE} \
php /app/bin/console messenger:consume --all --time-limit=3600 -vv
ExecStop=/usr/bin/docker stop domain-watchdog-worker
Restart=always
RestartSec=5s
[Install]
WantedBy=multi-user.target
- name: dozzle-agent.service
enabled: true
contents: |
[Unit]
Description=Dozzle Agent
After=docker.service versions-fetch.service
Wants=versions-fetch.service
Requires=docker.service
[Service]
# Image tag comes from versions.env; see butane/versions-sync.yaml.
EnvironmentFile=/etc/versions.env
TimeoutStartSec=0
ExecStartPre=-/usr/bin/docker rm --force dozzle-agent
ExecStart=/usr/bin/docker run --name dozzle-agent \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 7007:7007 \
$${DOZZLE_IMAGE} agent
ExecStop=/usr/bin/docker stop dozzle-agent
Restart=always
RestartSec=5s
[Install]
WantedBy=multi-user.target
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
# Domain Watchdog's JWT keypair; the container runs as root.
- path: /var/lib/domain-watchdog/jwt
mode: 0700
files:
# Non-secret Domain Watchdog settings, shared by the web app and worker
# units (docker --env-file format: no quoting, values taken literally).
- path: /etc/domain-watchdog/app.env
mode: 0644
contents:
inline: |
APP_ENV=prod
SERVER_NAME=:80
HTTP_SECURE_COOKIE=true
SYMFONY_TRUSTED_PROXIES=private_ranges
SYMFONY_TRUSTED_HEADERS=x-forwarded-for,x-forwarded-host,x-forwarded-proto,x-forwarded-port
LOCK_DSN=redis://valkey:6379
MESSENGER_ASYNC_TRANSPORT_DSN=redis://valkey:6379/messages
MESSENGER_RDAP_LOW_TRANSPORT_DSN=redis://valkey:6379/messages-rdap-low
MESSENGER_RDAP_HIGH_TRANSPORT_DSN=redis://valkey:6379/messages-rdap-high
REGISTRATION_ENABLED=false
REGISTRATION_VERIFY_EMAIL=false
OAUTH_AUTHORIZATION_URL=https://auth.atlashorizon.net/api/oidc/authorization
OAUTH_TOKEN_URL=https://auth.atlashorizon.net/api/oidc/token
OAUTH_USERINFO_URL=https://auth.atlashorizon.net/api/oidc/userinfo
OAUTH_SCOPE=openid email profile
SSO_AUTO_REDIRECT=true
MAILER_DSN=null://null
# Markdown pages the frontend fetches from /content/<name>; a missing one
# renders as "Please create the /public/content/<name> file." Mounted
# read-only into the web app. ads.md is shown on the login page and is
# left empty on purpose (an empty file renders as nothing).
- path: /etc/domain-watchdog/content/home.md
mode: 0644
contents:
inline: |
# Domain Watchdog
Tracks our domains through RDAP and alerts on expiry, transfers and
other registry changes.
Add domains under **Tracking > Watchlists**, and attach a
notification connector to a watchlist to be alerted.
- path: /etc/domain-watchdog/content/faq.md
mode: 0644
contents:
inline: |
# FAQ
**Who can log in?** Anyone allowed through Authelia for this app.
Your account is created on first login.
**How often are domains checked?** The worker refreshes watched
domains from RDAP on a schedule; domains close to expiry are
checked more often.
- path: /etc/domain-watchdog/content/tos.md
mode: 0644
contents:
inline: |
# Terms of use
Private instance for Atlas Horizon use only.
- path: /etc/domain-watchdog/content/privacy.md
mode: 0644
contents:
inline: |
# Privacy
Stores your email address (from Authelia) and your watchlists.
Nothing is shared with third parties; RDAP lookups go directly to
the registries.
- path: /etc/domain-watchdog/content/ads.md
mode: 0644
contents:
inline: ""