vextoly/gitman

Fork of patx/gitman.

# GitMan nginx and backend isolation

Deployed November 3, 2026. These are the GitMan-specific production files; they
must not replace the shared nginx.conf or Wail virtual host.

| Repository file | VPS destination |
| --- | --- |
| gitman-http.conf | /etc/nginx/conf.d/gitman-hardening.conf |
| gitman-cloudflare-realip.conf | /etc/nginx/snippets/gitman-cloudflare-realip.conf |
| gitman-server.conf | /etc/nginx/snippets/gitman-server.conf |
| gitman-site.conf | /etc/nginx/sites-available/gitman |
| gitman.socket | /etc/systemd/system/gitman.socket |
| zz-private-socket.conf | /etc/systemd/system/gitman.service.d/zz-private-socket.conf |

The existing sites-enabled/gitman symlink is retained. The zz prefix deliberately
sorts after rollback-data.conf, whose older ExecStart binds TCP port 8080. Keep
that drop-in's active database/repository paths; do not revert to the inactive
default data directory.

## Backend isolation

Systemd creates /run/gitman/gunicorn.sock with mode 0660 and owner git:www-data.
Gunicorn inherits that socket from gitman.socket. nginx connects through it;
Gunicorn has no TCP listener, including on loopback. SocketBindDeny=any also
prevents the service and its children from explicitly binding IPv4/IPv6 ports.
Outbound networking, including DNS for Pages domain checks, remains available.
The socket is accessible only to its owner, nginx's group, and privileged users.

Both gitman.service and gitman.socket are enabled for reboot. Gunicorn's explicit
Unix bind is intentional: combining --bind fd://3 with systemd activation duplicates
fd 3 in the installed Gunicorn's socket list and fails startup.

## Request handling

- Ordinary forms: 64 KiB maximum; body idle timeout 30 seconds; proxy read/send
  idle timeouts 60 seconds; connect timeout 5 seconds.
- Git: 257 MiB maximum request (256 MiB repository cap plus framing allowance),
  body idle timeout 120 seconds, upstream read idle timeout 7200 seconds.
- Settings/bundle/chunk uploads: 257 MiB maximum request; body/send idle timeout
  120 seconds; upstream read idle timeout 7500 seconds. The legacy multipart
  upload shares the settings URL, so that URL requires the larger allowance.
  Application limits still enforce the actual 256 MiB bundle/repository cap.
- Commit details, pull request details, and archives: 300-second upstream read
  idle timeout for potentially expensive Git work.
- nginx buffers uploads before forwarding them, with finite body and concurrency
  limits. These timeouts measure inactivity, not total request duration.
- Requests: 20/second/IP plus burst 80. Auth POSTs: 10/minute/IP plus burst 10.
  Auth GETs do not consume the auth-specific bucket. Trailing-slash aliases share
  the auth bucket; normalized URI matching also covers encoded path characters.
  Excess returns HTTP 429.
- Active requests: 32/IP, 128/virtual host. Git additionally allows 4/IP and
  8/virtual host; settings/imports allow 2/IP and 4/virtual host.
- Forwarded headers are overwritten consistently for all upstream locations.
  CF-Connecting-IP is trusted only from official Cloudflare address ranges;
  arbitrary clients cannot choose their own rate-limit identity.
- HSTS applies to gitman.io and its www redirect, without includeSubDomains or
  preload. Wildcard Pages and custom domains retain their existing policy.
- TLS 1.2/1.3 and the Certbot TLS configuration are preserved. nginx version
  disclosure is disabled in the GitMan virtual hosts. Static assets get nosniff;
  application CSP/frame policies remain in the application to preserve Pages.
- GET, HEAD, and POST remain available. Unsupported methods return HTTP 405.
- GitMan access logs use /var/log/nginx/gitman-access.log and omit query strings,
  cookies, authorization headers, and request bodies. The existing nginx logrotate
  wildcard covers this file.

The Cloudflare list was checked against https://www.cloudflare.com/ips-v4 and
https://www.cloudflare.com/ips-v6 at deployment. Recheck both when maintaining
these files; do not replace this allowlist with trust for all addresses.

## Deployment and rollback

Back up nginx and the GitMan systemd units before installation. Install the files
with root ownership and mode 0644, retaining the enabled vhost symlink. Run
nginx -t, systemctl daemon-reload, and systemd-analyze verify gitman.service
gitman.socket before activation. Changing the backend transport requires stopping
and starting GitMan, then reloading nginx. Enable gitman.socket after successful
checks. A normal nginx-only update needs a syntax check and graceful reload.

Production backups:

- /root/gitman-releases/before-nginx-hardening-20261103T051754Z/nginx-config.tar.gz
  contains the original nginx configuration; baseline.json records protected
  configuration hashes, service PIDs, and Pages response hashes.
- /root/gitman-releases/before-private-socket-20261103T052303Z/config.tar.gz contains
  nginx and the GitMan unit configuration immediately before socket isolation.

For socket rollback, stop GitMan and gitman.socket, disable the new socket unit,
restore the pre-socket nginx upstream snippet, remove zz-private-socket.conf and
gitman.socket, daemon-reload, start GitMan, validate nginx, and reload nginx.
This restores the former loopback-only TCP backend while retaining nginx limits.
The deployment scripts performed this rollback automatically on failed checks.

## Verified on the live VPS

- nginx syntax and systemd unit verification; all relevant services active.
- Public HTTPS, auth pages, authenticated profile/edit profile/notifications,
  and older notification batches.
- Exact before/after bodies and statuses for user Pages, project Pages, and
  custom-domain Pages, including voldservices.com; no new HSTS on these hosts.
- Real Git clone and fetch through nginx; Git protocol v2 and push auth challenge.
- HTTP 413 for forms above 64 KiB and Git/import requests above 257 MiB, without
  transmitting oversized bodies; 1 MiB transfers accepted at the nginx boundary.
- A real 1 MiB chunk request reaches the app's CSRF rejection guard. No production
  repository import or credential change was needed for these probes.
- Repeated invalid-CSRF auth POSTs trigger nginx HTTP 429 even when the caller
  varies forged Cloudflare and forwarded-IP headers; application password-failure
  counters are not exercised by these probes.
- Held requests enforce both per-IP and per-vhost Git/import concurrency caps.
- Public Cloudflare traffic is attributed to a non-Cloudflare client address.
- nginx's user can use the private socket; an unrelated local user cannot.
- No Gunicorn TCP listener remains; port 8080 refuses local connections and an
  external connection probe is blocked. A transient unit with the same cgroup
  policy confirms explicit TCP binding is denied by the kernel.
- Shared nginx.conf and Wail's configuration hashes are unchanged. Wail's process
  was not restarted, and its HTTPS endpoint still returns HTTP 200.