Native VM install kit: systemd units, launcher, install and upgrade scripts; backups, restore and doctor without docker exec; DEPLOY.md VM, NPM and NFS sections

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-30 18:08:48 -06:00
co-authored by Claude Opus 5.5
parent f025479358
commit 45a1d1a42a
22 changed files with 2357 additions and 413 deletions
@@ -0,0 +1,70 @@
# Packet Station: the web app (uvicorn on port 8000, the API and the built frontend).
# Installed by deploy/vm/install.sh into /etc/systemd/system. See docs/DEPLOY.md, "Native VM install".
# NPM on docker-1 is the only client allowed on port 8000 (packetstation-firewall.service).
[Unit]
Description=Packet Station web app
Documentation=file:///opt/packetstation/current/docs/DEPLOY.md
Requires=packetstation-migrate.service
# Wants, not Requires: a restart of nftables.service must not restart the app. ExecStartPre checks the table.
Wants=packetstation-firewall.service
After=packetstation-migrate.service packetstation-firewall.service [email protected] network-online.target
Wants=[email protected] network-online.target
StartLimitIntervalSec=300
StartLimitBurst=5
[Service]
Type=exec
User=packetstation
Group=packetstation
EnvironmentFile=/etc/packetstation/packetstation.env
# The app's secrets only. It never gets the VISUAL or M-Files password.
LoadCredential=postgres_app_password:/etc/packetstation/secrets/postgres_app_password
LoadCredential=ldap_bind_password:/etc/packetstation/secrets/ldap_bind_password
LoadCredential=session_key:/etc/packetstation/secrets/session_key
LoadCredential=seal_key:/etc/packetstation/secrets/seal_key
# The app never listens without the firewall table in place ("+": this check runs as root).
ExecStartPre=+/usr/sbin/nft list table inet packetstation
ExecStart=/opt/packetstation/current/deploy/vm/ps-launch.sh app
SyslogIdentifier=packetstation-app
Restart=on-failure
RestartSec=5s
TimeoutStopSec=30s
WorkingDirectory=/
UMask=0027
MemoryMax=1G
LimitNOFILE=16384
# Sandbox. Read-only file system except the assets and the archive mount.
# The leading "-" lets the app start while the archive is not mounted (ARCHIVE_ROOT unset).
ReadWritePaths=/var/lib/packetstation/assets
ReadWritePaths=-/mnt/packetstation-archive
InaccessiblePaths=/etc/packetstation/secrets
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
ProtectProc=invisible
# AF_NETLINK: glibc's getaddrinfo asks the kernel for the local addresses through it.
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
RemoveIPC=yes
CapabilityBoundingSet=
AmbientCapabilities=
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
# Not set: MemoryDenyWriteExecute. pypdfium2 goes through ctypes, whose callbacks may need
# writable and executable memory. Turn it on only after a filing and a print pass on the test VM.
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,21 @@
# Packet Station: only NPM on docker-1 (TRUSTED_PROXY_HOST) and the VM itself may reach port 8000.
# Loads /etc/packetstation/packetstation.nft, which deploy/vm/install.sh writes from the env file.
# The table is Packet Station's own (inet packetstation); other rules on the VM are left alone.
[Unit]
Description=Packet Station firewall for the app port
Documentation=file:///opt/packetstation/current/docs/DEPLOY.md
Before=network-pre.target packetstation-app.service
Wants=network-pre.target
# A restart of nftables.service flushes the whole ruleset. Load the table again after it.
After=nftables.service
PartOf=nftables.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/sbin/nft -f /etc/packetstation/packetstation.nft
ExecReload=/usr/sbin/nft -f /etc/packetstation/packetstation.nft
ExecStop=-/usr/sbin/nft delete table inet packetstation
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,52 @@
# Packet Station: SQL migrations, once per boot and once per upgrade, before the app and the worker.
# Installed by deploy/vm/install.sh into /etc/systemd/system. See docs/DEPLOY.md, "Native VM install".
# ps-upgrade restarts this unit after switching /opt/packetstation/current to a new release.
[Unit]
Description=Packet Station database migrations
Documentation=file:///opt/packetstation/current/docs/DEPLOY.md
Wants=[email protected] network-online.target
After=[email protected] network-online.target
Before=packetstation-app.service packetstation-worker.service
ConditionPathExists=/opt/packetstation/current/deploy/vm/ps-launch.sh
[Service]
Type=oneshot
RemainAfterExit=yes
User=packetstation
Group=packetstation
EnvironmentFile=/etc/packetstation/packetstation.env
# migrate needs both passwords: it logs in as ps_migrate and grants to ps_app.
LoadCredential=postgres_migrate_password:/etc/packetstation/secrets/postgres_migrate_password
LoadCredential=postgres_app_password:/etc/packetstation/secrets/postgres_app_password
ExecStart=/opt/packetstation/current/deploy/vm/ps-launch.sh migrate
SyslogIdentifier=packetstation-migrate
TimeoutStartSec=15min
WorkingDirectory=/
UMask=0027
# Sandbox. The whole file system is read-only; migrate writes nothing outside PostgreSQL.
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
InaccessiblePaths=/etc/packetstation/secrets
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
ProtectProc=invisible
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
RemoveIPC=yes
CapabilityBoundingSet=
AmbientCapabilities=
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
# No [Install]: the app and the worker pull this unit in (Requires=).
@@ -0,0 +1,67 @@
# Packet Station: the worker (VISUAL feed, drawings, AD reconcile, integrity, backup check,
# notifications, archive copies). Installed by deploy/vm/install.sh into /etc/systemd/system.
# On production its first start is Spencer's approval of the scheduled VISUAL polling.
[Unit]
Description=Packet Station worker
Documentation=file:///opt/packetstation/current/docs/DEPLOY.md
Requires=packetstation-migrate.service
After=packetstation-migrate.service [email protected] network-online.target
Wants=[email protected] network-online.target
StartLimitIntervalSec=300
StartLimitBurst=5
[Service]
Type=exec
User=packetstation
Group=packetstation
EnvironmentFile=/etc/packetstation/packetstation.env
LoadCredential=postgres_app_password:/etc/packetstation/secrets/postgres_app_password
LoadCredential=visualkit_password:/etc/packetstation/secrets/visualkit_password
LoadCredential=mfileskit_password:/etc/packetstation/secrets/mfileskit_password
LoadCredential=ldap_bind_password:/etc/packetstation/secrets/ldap_bind_password
LoadCredential=seal_key:/etc/packetstation/secrets/seal_key
ExecStart=/opt/packetstation/current/deploy/vm/ps-launch.sh worker
SyslogIdentifier=packetstation-worker
# /run/packetstation holds the heartbeat file (WORKER_BEAT_FILE, set by ps-launch.sh).
RuntimeDirectory=packetstation
RuntimeDirectoryMode=0755
Restart=on-failure
RestartSec=10s
TimeoutStopSec=60s
WorkingDirectory=/
UMask=0027
MemoryMax=1G
# Sandbox. Read-only file system except the assets and the archive mount. The backup folder
# (BACKUP_DIR) is only read, so it needs no entry here.
ReadWritePaths=/var/lib/packetstation/assets
ReadWritePaths=-/mnt/packetstation-archive
InaccessiblePaths=/etc/packetstation/secrets
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
ProtectProc=invisible
# AF_NETLINK: glibc's getaddrinfo asks the kernel for the local addresses through it.
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
RemoveIPC=yes
CapabilityBoundingSet=
AmbientCapabilities=
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
# Not set: MemoryDenyWriteExecute (see packetstation-app.service).
[Install]
WantedBy=multi-user.target