RatMinimal web framework

Sample configs

Minimal, copyable templates for the most common deploy shapes. Each one is the smallest config that satisfies the publishing checklist, with no extras and no aspirational fields.

Caddyfile

TLS, reverse proxy, security headers

Caddy fronts the Rat binary on 127.0.0.1, terminates TLS with an automatic Let's Encrypt cert, and forwards X-Forwarded-Proto: https so the HSTS header fires. The header stanza strips any upstream Server / X-Powered-By and adds the baseline policy headers.

# /etc/caddy/Caddyfile
your-domain.com { encode zstd gzip reverse_proxy 127.0.0.1:1455 { header_up X-Real-IP {remote_host} header_up X-Forwarded-Proto https flush_interval -1 } header { -Server -X-Powered-By Strict-Transport-Security "max-age=63072000; includeSubDomains" X-Frame-Options "SAMEORIGIN" X-Content-Type-Options "nosniff" Referrer-Policy "strict-origin-when-cross-origin" Permissions-Policy "camera=(), microphone=(), geolocation=()" } @disallowed_methods not method GET HEAD POST respond @disallowed_methods 405 }

Notes: flush_interval -1 keeps SSE channels streaming without proxy buffering. The matcher implements the verb allowlist at the edge. Rat's own security headers also fire; duplicate values are harmless and let either layer be the source of truth.

systemd unit

Runs the binary under a dedicated user

The service runs Rat as an unprivileged user with the working directory pinned to the project root. Secrets come from an EnvironmentFile outside the unit so the unit itself can stay in source control without leaking values.

# /etc/systemd/system/rat-site.service
[Unit]
Description=Rat docs site
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=rat
Group=rat
WorkingDirectory=/srv/rat-site
EnvironmentFile=/etc/rat-site.env
ExecStart=/srv/rat-site/rat start 1455 prod
Restart=on-failure
RestartSec=5

# Sandboxing - tighten further per your threat model.
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/srv/rat-site
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictNamespaces=true
RestrictRealtime=true
LockPersonality=true

[Install]
WantedBy=multi-user.target

ReadWritePaths stays narrow so the binary can write its SQLite file and uploads directory without seeing the rest of the filesystem. Reload with systemctl daemon-reload after edits, then systemctl enable --now rat-site.

Env bundle

Mode 0600, owned by the service user

The file referenced by the unit's EnvironmentFile. Generate the two secrets with any 32-byte source; openssl rand -hex 32 works.

# /etc/rat-site.env  (chmod 0600 root:root or service-user)
RAT_MODE=prod
RAT_SESSION_SECRET=<openssl rand -hex 32>
RAT_ENCRYPTION_KEY=<openssl rand -hex 32>

# Optional: pin the Python tail interpreter when PATH-based
# discovery is fragile. This matters under LocalSystem on
# Windows, or under a stripped-PATH systemd unit.
# RAT_PYTHON=/usr/bin/python3

IIS + ARR

Windows deploy pattern

For Windows hosts that already own 80/443 with IIS, ARR is the equivalent of the Caddy story. The web.config below sits in the site's physical path next to nothing else; IIS forwards everything to the Rat binary on 127.0.0.1, and an outbound rewrite scrubs the X-Powered-By ARR injects downstream.

<!-- web.config -->
<configuration>
  <system.webServer>
    <rewrite>
      <rules>
        <rule name="canonicalize-www-to-apex" stopProcessing="true">
          <match url="(.*)" />
          <conditions logicalGrouping="MatchAll">
            <add input="{HTTP_HOST}" pattern="^www\.your-domain\.com$" />
            <add input="{REQUEST_URI}" pattern="^/\.well-known/acme-challenge/" negate="true" />
          </conditions>
          <action type="Redirect" url="https://your-domain.com/{R:1}" redirectType="Permanent" />
        </rule>
        <rule name="https-redirect" stopProcessing="true">
          <match url="(.*)" />
          <conditions logicalGrouping="MatchAll">
            <add input="{HTTPS}" pattern="off" />
            <add input="{REQUEST_URI}" pattern="^/\.well-known/acme-challenge/" negate="true" />
          </conditions>
          <action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" />
        </rule>
        <rule name="reverse-proxy" stopProcessing="true">
          <match url="(.*)" />
          <conditions>
            <add input="{REQUEST_URI}" pattern="^/\.well-known/acme-challenge/" negate="true" />
          </conditions>
          <serverVariables>
            <set name="HTTP_X_FORWARDED_PROTO" value="https" />
            <set name="HTTP_X_FORWARDED_FOR" value="{REMOTE_ADDR}" />
            <set name="HTTP_X_FORWARDED_HOST" value="{HTTP_HOST}" />
          </serverVariables>
          <action type="Rewrite" url="http://127.0.0.1:1455/{R:1}" />
        </rule>
      </rules>
      <outboundRules>
        <rule name="strip-powered-by">
          <match serverVariable="RESPONSE_X-Powered-By" pattern=".*" />
          <action type="Rewrite" value="" />
        </rule>
        <rule name="strip-server">
          <match serverVariable="RESPONSE_Server" pattern=".*" />
          <action type="Rewrite" value="" />
        </rule>
      </outboundRules>
    </rewrite>
    <security>
      <requestFiltering removeServerHeader="true">
        <verbs allowUnlisted="false">
          <add verb="GET" allowed="true" />
          <add verb="HEAD" allowed="true" />
          <add verb="POST" allowed="true" />
        </verbs>
      </requestFiltering>
    </security>
    <proxy enabled="true" preserveHostHeader="true" responseBufferLimit="0" />
  </system.webServer>
</configuration>

Bind both apex and www on the IIS site (ports 80 + 443) and request a SAN cert covering both. WACS's IIS source plugin reads bindings to decide which hostnames go on the cert, so a missing www binding produces a single-name cert and visitors hitting www see SEC_E_WRONG_PRINCIPAL from whatever default-binding cert IIS falls back to. The first rule then 301s www to apex so search engines and links converge on one canonical URL.

The ACME-challenge exclusion is critical: without it, future Let's Encrypt renewals get a 301 to HTTPS or a 404 from Rat instead of the challenge file. responseBufferLimit="0" keeps SSE streaming. The verb allowlist mirrors the Caddy version. removeServerHeader="true" plus the outbound Server scrub handles the two layers where IIS leaks its product banner. Service registration via NSSM is one possible path; any service supervisor works.

See also Publishing a Rat app · Threat model · CLI