# nmg-api, running as nmg, needs to reload the Postfix
# configuration after `postconf -e` writes new values to main.cf. We
# grant exactly that — no shells, no wildcards, no other binaries.
# Additional reload paths can be added here as new etappes integrate
# more of the mail stack.
nmg ALL=(root) NOPASSWD: /usr/sbin/postfix reload
# freshclam mit unserer Custom-Config triggern (sofortiger Refresh
# nach Source-CRUD, statt 6h auf den Timer zu warten). Pfad gepinnt
# damit das nicht für andere Configs missbraucht werden kann.
nmg ALL=(root) NOPASSWD: /usr/bin/freshclam --config-file=/etc/nmg/clamav-extras.conf --quiet
# `postfix check` is used by /diagnostics — Postfix refuses to run any
# subcommand as a non-root user ("the postfix command is reserved for
# the superuser"), so the diagnose call has to go via sudo. Argument
# fixed to "check" so this cannot be used to start/stop/flush.
nmg ALL=(root) NOPASSWD: /usr/sbin/postfix check
nmg ALL=(root) NOPASSWD: /bin/systemctl reload postfix
nmg ALL=(root) NOPASSWD: /bin/systemctl reload postfix@-.service
nmg ALL=(root) NOPASSWD: /bin/systemctl reload rspamd
nmg ALL=(root) NOPASSWD: /bin/systemctl reload rspamd.service
nmg ALL=(root) NOPASSWD: /bin/systemctl restart rspamd
nmg ALL=(root) NOPASSWD: /bin/systemctl restart rspamd.service
# Dovecot reload nach /etc/nmg/sasl_accounts-Rewrite. Submission-Auth
# (Postfix:587) konsultiert Dovecot über UNIX-Socket — Reload pickt
# neue/entfernte Domain-Credentials sofort auf.
nmg ALL=(root) NOPASSWD: /bin/systemctl reload dovecot
nmg ALL=(root) NOPASSWD: /bin/systemctl reload dovecot.service
# ACME flow: brief nginx stop + standalone certbot + nginx start, plus
# renewal. Certbot args differ between issue + renew — hence the
# dedicated lines with the command arg constrained to the subcommand.
nmg ALL=(root) NOPASSWD: /bin/systemctl stop nginx
nmg ALL=(root) NOPASSWD: /bin/systemctl start nginx
nmg ALL=(root) NOPASSWD: /bin/systemctl reload nginx
nmg ALL=(root) NOPASSWD: /usr/bin/certbot certonly *
nmg ALL=(root) NOPASSWD: /usr/bin/certbot renew *
# Postfix queue management from the admin UI. `postsuper` requires root
# because it manipulates the spool directory; `postqueue -j` and
# `postqueue -f` are safe to run as nmg but go via sudo here for a
# single audit/logging path.
nmg ALL=(root) NOPASSWD: /usr/sbin/postqueue -j
nmg ALL=(root) NOPASSWD: /usr/sbin/postqueue -p
nmg ALL=(root) NOPASSWD: /usr/sbin/postqueue -f
# postqueue -s <site>: domain-spezifischer Flush — vom Scheduler genutzt
# um auf Backup-MX-Domains nach Ablauf der backup_mx_hold_time die
# deferred-Mails an die primary-MX zu schubsen.
nmg ALL=(root) NOPASSWD: /usr/sbin/postqueue -s *
# YARA-Recompile aus dem Scheduler. compile.sh selbst ist im Paket
# (root:root 0755), läuft als root und ruft yarac. Audit 2026-04-29:
# Setting yara_update_hours steuert das jetzt dynamisch statt eines
# festen 12h-Systemd-Timers.
nmg ALL=(root) NOPASSWD: /usr/share/nmg/yara-base/compile.sh
nmg ALL=(root) NOPASSWD: /usr/sbin/postsuper -d *
nmg ALL=(root) NOPASSWD: /usr/sbin/postsuper -r *
nmg ALL=(root) NOPASSWD: /usr/sbin/postsuper -h *
nmg ALL=(root) NOPASSWD: /usr/sbin/postsuper -H *
# postcat reads queue entries. Required for the admin Quarantine
# UI's Hold-Queue preview + the headers-only enrichment pass +
# the "Train as Spam/Ham" buttons that pipe the body to rspamc.
# The queue dir is postfix-private (drwx------ postfix root) so
# this NEEDS root; nmg can't read it directly. Two flag-forms:
#   -q  <qid>  full message (UI preview, training)
#   -hq <qid>  headers only (List enrichment for Subject+Score)
nmg ALL=(root) NOPASSWD: /usr/sbin/postcat -q *
nmg ALL=(root) NOPASSWD: /usr/sbin/postcat -hq *
# Auto-update toggle: writes/removes a single pinned apt preferences
# file. Paths are fixed — no wildcards — so this cannot be used to
# clobber or remove any other file under /etc/apt/apt.conf.d.
nmg ALL=(root) NOPASSWD: /usr/bin/tee /etc/apt/apt.conf.d/52nmg-auto-updates
nmg ALL=(root) NOPASSWD: /bin/rm -f /etc/apt/apt.conf.d/52nmg-auto-updates
# TLS self-service: postconf writes myhostname on FQDN change, and
# certbot.timer is disabled when an admin uploads a custom cert so
# automated renewals don't overwrite their file.
nmg ALL=(root) NOPASSWD: /usr/sbin/postconf -e myhostname=*
nmg ALL=(root) NOPASSWD: /bin/systemctl stop certbot.timer
nmg ALL=(root) NOPASSWD: /bin/systemctl disable certbot.timer
# certbot creates /etc/letsencrypt/{live,archive} as 0700 root:root so
# nginx (www-data) and nmg-api (nmg) can't follow the symlinks in
# /etc/nmg/tls/. Loosening the two dirs to 0755 is safe: fullchain.pem
# is public and privkey.pem stays 0640 root:root either way.
nmg ALL=(root) NOPASSWD: /bin/chmod 755 /etc/letsencrypt/live
nmg ALL=(root) NOPASSWD: /bin/chmod 755 /etc/letsencrypt/archive
# Self-upgrade: fixed helper at /usr/lib/nmg/nmg-self-upgrade — root-
# owned and shipped by the .deb — that runs `apt install --only-upgrade
# nmg` inside a transient systemd-run unit. No arguments are passed so
# this cannot be repurposed for arbitrary apt operations.
nmg ALL=(root) NOPASSWD: /usr/lib/nmg/nmg-self-upgrade
# apt list refresh runs in nmg-api before /system/package-versions so
# the update banner reflects what was just uploaded to the repo. The
# nmg user cannot write to /var/lib/apt/lists by default, so the call
# goes through sudo. Argument is fixed to "update -qq" — no shells,
# no install actions allowed via this entry.
nmg ALL=(root) NOPASSWD: /usr/bin/apt-get update -qq
# nftables peer-IP set management. nmg-api adds a peer's IP to the
# inet nmg peer_ipv4 / peer_ipv6 set on /cluster/register so future
# Active-Active KeyDB / rspamd-fuzzy traffic can flow. Wildcards on
# the value are needed because the IP is dynamic; the set name is
# pinned so this can't be used to add other rules.
nmg ALL=(root) NOPASSWD: /usr/sbin/nft add element inet nmg peer_ipv4 *
nmg ALL=(root) NOPASSWD: /usr/sbin/nft add element inet nmg peer_ipv6 *
nmg ALL=(root) NOPASSWD: /usr/sbin/nft delete element inet nmg peer_ipv4 *
nmg ALL=(root) NOPASSWD: /usr/sbin/nft delete element inet nmg peer_ipv6 *
# Custom firewall ruleset reload. nmg-api regenerates the file from
# firewall_rules and runs nft -f to load it. Path is fixed — no other
# nft script can be loaded via this entry.
nmg ALL=(root) NOPASSWD: /usr/sbin/nft -f /etc/nftables.d/nmg-custom.nft
# Cluster-join from the setup wizard. nmg-ctl rewrites root-owned
# files (/etc/nmg/secret/peer.crt, /var/lib/nmg/cluster.json) and
# triggers a self-restart, none of which the nmg user can do under
# ProtectSystem=strict. Wildcards on the args are ok here because the
# binary is fixed and itself only accepts a small grammar.
nmg ALL=(root) NOPASSWD: /usr/bin/nmg-ctl cluster-init *
nmg ALL=(root) NOPASSWD: /usr/bin/nmg-ctl cluster-join *
nmg ALL=(root) NOPASSWD: /bin/systemctl restart nmg-api
