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:
201
generic.md
201
generic.md
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user