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) <noreply@anthropic.com>
This commit is contained in:
2026-04-22 11:50:00 +03:00
parent bec0ba92c4
commit ecfefa6433

View File

@@ -22,7 +22,7 @@ Projects are Rust cargo workspaces. The repository root contains:
│ ├── <app>-api/ # binary: REST / JSON / WebSocket daemon
│ ├── <app>-worker/ # binary: long-running processor / queue consumer
│ └── <app>-cli/ # binary: operator / admin CLI
├── <web/dashboard/ui>/ # 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/
│ ├── <app>-api.socket # if socket-activated
│ ├── <app>-worker.service
│ ├── <app>-indexer.timer
── <app>-indexer.service
── <app>-indexer.service
│ └── <app>.sysusers.conf # systemd-sysusers drop-in
├── firewalld/
│ ├── <app>-api.xml # named firewalld service per §9
│ └── <app>-worker.xml
├── selinux/ # only if custom policy is required
│ ├── <app>.te
│ └── <app>.fc
├── nginx/
│ └── <app>.<site>.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/<app>`, `/var/lib/<app>` 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/<app>.sysusers.conf`:
```
#Type Name ID GECOS Home directory Shell
u <app> - "<App> service account" /var/lib/<app> /usr/sbin/nologin
```
`deploy.sh` installs this to `/etc/sysusers.d/<app>.conf` and runs `systemd-sysusers` on the target. This is idempotent and survives package reinstalls.
### Directory ownership
Services typically need:
- `/etc/<app>/` — config (root:<app>, 0750; files 0640) so the daemon can read but not write.
- `/var/lib/<app>/` — mutable state (<app>:<app>, 0750).
- `/var/log/<app>/` — 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=<app>
Group=<app>
ExecStart=/usr/local/bin/<app>-api --config /etc/<app>/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/<app>
# 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/<app>-<component>.xml`:
```xml
<?xml version="1.0" encoding="utf-8"?>
<service>
<short><app>-api</short>
<description>REST/WebSocket API for <app></description>
<port protocol="tcp" port="8443"/>
<!-- multiple ports fine if the app needs them -->
<port protocol="tcp" port="8444"/>
</service>
```
### What `deploy.sh` must do
For each component with a firewalld service definition:
1. `rsync` the XML to `/etc/firewalld/services/<app>-<component>.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=<zone> --query-service=<app>-<component>
```
4. If not, enable it persistently **and** in the runtime config:
```
firewall-cmd --permanent --zone=<zone> --add-service=<app>-<component>
firewall-cmd --zone=<zone> --add-service=<app>-<component>
```
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 (`<port port="x-y" />`, `<source address="..."/>`). 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/<app>/`, config under `/etc/<app>/`, 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/<app>/bin(/.*)?'
semanage fcontext -a -t etc_t '/opt/<app>/etc(/.*)?'
semanage fcontext -a -t var_lib_t '/opt/<app>/var(/.*)?'
restorecon -Rv /opt/<app>
```
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 <port>` 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/<app>.te` (and `.fc`, `.if` as needed). Build and install at deploy time:
```
checkmodule -M -m -o <app>.mod <app>.te
semodule_package -o <app>.pp -m <app>.mod -f <app>.fc
semodule -i <app>.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/<app>.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.<site>.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/<app>` 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.
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.