From ecfefa6433e44061ad7288e0ee88dd7c315a6e56 Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Wed, 22 Apr 2026 11:50:00 +0300 Subject: [PATCH] docs(generic): add Fedora deployment sections for sysusers, firewalld, and SELinux Expand generic.md with detailed guidance on service account creation via systemd-sysusers, named firewalld service definitions, and SELinux policy management. Update deploy.sh responsibilities, asset layout, and conventions summary to reflect the new requirements. Co-Authored-By: Claude Opus 4.7 (1M context) --- generic.md | 201 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 185 insertions(+), 16 deletions(-) diff --git a/generic.md b/generic.md index e78f9ab..9247243 100644 --- a/generic.md +++ b/generic.md @@ -22,7 +22,7 @@ Projects are Rust cargo workspaces. The repository root contains: │ ├── -api/ # binary: REST / JSON / WebSocket daemon │ ├── -worker/ # binary: long-running processor / queue consumer │ └── -cli/ # binary: operator / admin CLI -├── / # Vite + React + SWC + TS frontend (when applicable) +├── web/ # Vite + React + SWC + TS frontend (when applicable) ├── asset/ # deployment artifacts (see §6) ├── script/ # deploy.sh and related operational scripts └── README.md @@ -193,7 +193,14 @@ asset/ │ ├── -api.socket # if socket-activated │ ├── -worker.service │ ├── -indexer.timer -│ └── -indexer.service +│ ├── -indexer.service +│ └── .sysusers.conf # systemd-sysusers drop-in +├── firewalld/ +│ ├── -api.xml # named firewalld service per §9 +│ └── -worker.xml +├── selinux/ # only if custom policy is required +│ ├── .te +│ └── .fc ├── nginx/ │ └── ..conf # per server_name configs ├── config/ @@ -213,7 +220,7 @@ environments: api: hosts: [oolon.hanzalova.internal] config: - bind: 0.0.0.0:8080 + bind: 127.0.0.1:8080 log_level: info worker: hosts: [gramathea.kosherinata.internal, oolon.hanzalova.internal] @@ -227,7 +234,7 @@ environments: api: hosts: [quadbrat.hanzalova.internal] config: - bind: 0.0.0.0:8080 + bind: 127.0.0.1:8080 log_level: debug # ... ``` @@ -258,8 +265,13 @@ A bash script with a stable CLI: - For each `(component, host)` pair: 1. Build the artifact locally (cargo build release, vite build, etc.) if not already built for the current commit. 2. Resolve secrets from `pass` (or the project's configured secret backend) and render config templates. - 3. `rsync` binary + rendered config + systemd units to the target host over ssh (quantum-safe key exchange). - 4. `ssh` to the target to `systemctl daemon-reload` and restart the unit(s). + 3. `rsync` binary + rendered config + systemd units + sysusers drop-in + firewalld XML + any SELinux assets to the target host over ssh (quantum-safe key exchange). + 4. On the target, in this order: + a. `systemd-sysusers` to create the service account if missing (§8). + b. Create/chown `/etc/`, `/var/lib/` with correct modes. + c. `restorecon -R` on installed paths; apply `semanage` changes and load any policy module (§10). + d. Install firewalld service XML, `firewall-cmd --reload`, ensure the service is enabled in the appropriate zone, persistently and at runtime (§9). + e. `systemctl daemon-reload` and restart the unit(s). 5. Verify health (HTTP probe for api, `systemctl is-active` for all). - Exit non-zero on any failure. Report per-host status at the end. @@ -271,9 +283,162 @@ A bash script with a stable CLI: --- -## 8. Infrastructure Context +## 8. Deployment: Service Accounts -This is the environment these apps deploy into. Implementation should assume it. +All targets run **Fedora Server (current stable, 43 at time of writing)**. Claude Code should assume this and target the idiomatic Fedora way of doing things. + +Services run as **dedicated non-root system users** by default. Root is an exception requiring explicit justification (e.g., a service that genuinely needs to bind < 1024 without `CAP_NET_BIND_SERVICE`, manage kernel modules, or similar). + +### Creating the service account +Ship a `sysusers.d` drop-in with the app and let systemd-sysusers create the user at deploy time. Place it at `asset/systemd/.sysusers.conf`: + +``` +#Type Name ID GECOS Home directory Shell +u - " service account" /var/lib/ /usr/sbin/nologin +``` + +`deploy.sh` installs this to `/etc/sysusers.d/.conf` and runs `systemd-sysusers` on the target. This is idempotent and survives package reinstalls. + +### Directory ownership +Services typically need: +- `/etc//` — config (root:, 0750; files 0640) so the daemon can read but not write. +- `/var/lib//` — mutable state (:, 0750). +- `/var/log//` — only if the service logs somewhere other than journald (rare; prefer journald). + +`deploy.sh` must create these with correct ownership and modes, not rely on the service creating them at runtime. + +### systemd unit hardening +Unit files in `asset/systemd/` should use the user, not run as root, and include the standard hardening knobs unless a specific feature prevents it: + +```ini +[Service] +Type=notify +User= +Group= +ExecStart=/usr/local/bin/-api --config /etc//config.toml + +# Hardening — enable unless the service genuinely needs otherwise +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=true +PrivateTmp=true +PrivateDevices=true +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectControlGroups=true +RestrictRealtime=true +RestrictSUIDSGID=true +LockPersonality=true +MemoryDenyWriteExecute=true +SystemCallArchitectures=native + +# Writable paths (minimum necessary) +ReadWritePaths=/var/lib/ + +# Network +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 +``` + +If a setting breaks the service, relax only that one — don't disable hardening wholesale. + +### Privilege exceptions +If the service must run as root or with extra capabilities, document the reason in a comment at the top of the unit file. Prefer narrow `AmbientCapabilities=` (e.g., `CAP_NET_BIND_SERVICE`) over full root. + +--- + +## 9. Deployment: Firewall (firewalld) + +All hosts run `firewalld`. Every service that listens on a port must ship a **named firewalld service definition** rather than opening bare ports in a zone. The service name matches the systemd unit name (minus `.service`). + +### Why named services +Named services are self-documenting (`firewall-cmd --list-services` tells you what's actually running), removable atomically on app decommission, and survive zone reassignment without reconfiguration. + +### Shipping the definition +Place the XML in `asset/firewalld/-.xml`: + +```xml + + + -api + REST/WebSocket API for + + + + +``` + +### What `deploy.sh` must do +For each component with a firewalld service definition: + +1. `rsync` the XML to `/etc/firewalld/services/-.xml` on the target. +2. `firewall-cmd --reload` to pick up the new definition. +3. Check if the service is already enabled in the target zone (default zone unless the manifest specifies otherwise): + ``` + firewall-cmd --zone= --query-service=- + ``` +4. If not, enable it persistently **and** in the runtime config: + ``` + firewall-cmd --permanent --zone= --add-service=- + firewall-cmd --zone= --add-service=- + ``` +5. On component removal (future concern), the reverse: `--remove-service` then delete the XML. + +Steps must be idempotent — re-running a deploy is a no-op on the firewall layer if the service is already installed and enabled. + +### Zone selection +Most services bind to internal WireGuard interfaces. Put the WireGuard interface in a dedicated `internal` or `wg` zone and open services there. Public-facing services (rare — nginx is usually the only one) go in the default `public`/`FedoraServer` zone. The manifest may optionally specify a `zone:` per component; default to `internal` if unset. + +### Port ranges, ICMP, sources +If a service needs port ranges, ICMP types, or source-IP restrictions, put them in the same XML using firewalld's standard elements (``, ``). Don't split these across multiple named services. + +--- + +## 10. Deployment: SELinux + +All hosts run **SELinux in enforcing mode**. Deployments must either operate cleanly within the default targeted policy or ship the labels and policy modules they need. Running `setenforce 0` to "just get it working" is never acceptable, and Claude Code should flag any suggestion to do so. + +### Order of preference +Try these in order. Go no further down the list than necessary: + +1. **Fit the default policy.** Install binaries to `/usr/local/bin/` (or `/usr/bin/` for packaged apps), state under `/var/lib//`, config under `/etc//`, logs to journald. These paths already have sensible default labels and most Rust daemons will run unmodified under `unconfined_service_t` or `init_t`. +2. **Apply existing contexts with `semanage fcontext`.** When files land in non-standard paths, map them to an appropriate existing type: + ``` + semanage fcontext -a -t bin_t '/opt//bin(/.*)?' + semanage fcontext -a -t etc_t '/opt//etc(/.*)?' + semanage fcontext -a -t var_lib_t '/opt//var(/.*)?' + restorecon -Rv /opt/ + ``` +3. **Use booleans** for common permissions the service needs (e.g., `setsebool -P httpd_can_network_connect on` if nginx needs to reach the API). Document every boolean flipped in the deployment. +4. **Register non-standard ports.** If the API binds to a port not already known to SELinux, label it: + ``` + semanage port -a -t http_port_t -p tcp 8443 # if not already labelled + ``` + Check first with `semanage port -l | grep ` and skip if the label is correct. +5. **Ship a custom policy module** only when the above don't cover it. Place sources in `asset/selinux/.te` (and `.fc`, `.if` as needed). Build and install at deploy time: + ``` + checkmodule -M -m -o .mod .te + semodule_package -o .pp -m .mod -f .fc + semodule -i .pp + ``` + Custom modules should be as narrow as possible. If the policy ends up allowing everything, it's wrong — generate rules from `audit2allow` only after confirming the denial is actually legitimate, never as a blanket suppression. + +### What `deploy.sh` must do +- After installing files, always run `restorecon -R` on their installation paths so filesystem labels match the policy. +- Apply `semanage fcontext` / `semanage port` / `setsebool` changes **permanently** (no runtime-only hacks). +- Load or reload any shipped policy module with `semodule -i`. +- Keep these operations idempotent. `semanage fcontext -a` on an already-registered path errors; deploy scripts should check with `semanage fcontext -l` first, or use `-m` (modify) with a guard. + +### Dev loop +During development on a test host, `ausearch -m AVC -ts recent` and `audit2why` are the primary tools for diagnosing denials. Capture the clean set of rules once stable, fold into `asset/selinux/.te`, and commit. Never leave a host with `permissive` mode set — if you set it during debugging, put it back before ending the session. + +### Podman quadlets +For containerised workloads: quadlets run confined under `container_t` by default. Bind mounts of host paths need `:Z` (private relabel) or `:z` (shared relabel) depending on whether the volume is shared across containers. Default to `:Z` unless sharing is required. + +--- + +## 11. Infrastructure Context + +This is the environment these apps deploy into. Claude Code should assume it. ### Network - Multi-site WireGuard mesh. Sites are numbered; host IPs follow `10..0.0/16` (currently `10.3.0.0/16` and `10.6.0.0/16`, but the second octet encodes the site and is the stable part). @@ -292,14 +457,15 @@ This is the environment these apps deploy into. Implementation should assume it. - nginx serves static frontends directly from `/var/www/` and reverse-proxies API traffic to the internal host:port from `manifest.yml`. ### Hosts -- Workstations and servers run the latest Fedora. -- Services run as dedicated non-root users created by RPM scaffolding or `sysusers.d` drop-ins shipped in `asset/systemd/`. -- SELinux enforcing. Any non-standard file paths or ports require a policy module shipped with the app. +- Fedora Server, current stable (43). Workstations run the same release. +- Services run as dedicated non-root users per §8. +- firewalld with named services per §9. +- SELinux enforcing per §10. - Podman quadlets for containerised workloads; bare-metal systemd units for native Rust binaries (preferred where feasible). --- -## 9. Code Quality and Tooling +## 12. Code Quality and Tooling ### Formatting and linting - `cargo fmt` on commit (pre-commit hook or CI gate). @@ -329,7 +495,7 @@ This is the environment these apps deploy into. Implementation should assume it. --- -## 10. Conventions Summary for Scaffolding and Implementation +## 13. Conventions Summary for Claude Code When scaffolding or extending a project: @@ -337,9 +503,12 @@ When scaffolding or extending a project: 2. Put new types in `entities`, new logic in `core`, new I/O in `data`. Binaries stay thin. 3. Add dependencies to the workspace root first, then reference with `dep.workspace = true`. 4. Version strings live in exactly one place — the workspace root. -5. Any new deployable component gets an entry in `asset/manifest.yml` and a systemd unit in `asset/systemd/` in the same change. +5. Any new deployable component gets an entry in `asset/manifest.yml`, a systemd unit in `asset/systemd/`, a sysusers drop-in, a firewalld service XML, and any required SELinux assets — in the same change. 6. Config templates go in `asset/config/` with `{{PLACEHOLDER}}` secrets. Never commit a rendered config. 7. Postgres connections are mTLS, passwordless. If writing connection code that accepts a password, stop and ask. 8. Frontend is Vite + React + SWC + TS, served as static assets from nginx. Rust web frameworks require a stated reason. -9. Prefer fewer dependencies. Prefer bare-metal systemd over containers unless there's a reason. -10. When unsure, ask — these preferences are defaults, not mandates, but deviations should be deliberate. \ No newline at end of file +9. Services run as dedicated non-root users with hardened systemd units per §8. Root requires explicit justification. +10. Every listening port gets a named firewalld service per §9. No bare `--add-port` calls. +11. SELinux stays enforcing. Work with the default policy first; ship a custom module only when necessary (§10). Never suggest `setenforce 0`. +12. Prefer fewer dependencies. Prefer bare-metal systemd over containers unless there's a reason. +13. When unsure, ask — these preferences are defaults, not mandates, but deviations should be deliberate.