385 lines
17 KiB
Plaintext
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: ""
|