# nginx configuration for cgit. # # This is a complete nginx.conf rather than a snippet for conf.d or # sites-enabled, so nothing here is included from elsewhere. Install it and # reload, or point nginx straight at it to try it out. # nginx -t -c /path/to/nginx.conf # check the syntax # nginx -c /path/to/nginx.conf # run it # # Paths assumed below, edit them to match your install. # cgit CGI binary /usr/lib/cgit/cgit.cgi # static assets /usr/share/cgit (cgit.css cgit.js cgit.png favicon.ico robots.txt) # cgit config /etc/cgitrc # public URL https://git.example.org/ (cgit at the domain root) # # cgit is one CGI executable. It learns the repository and the page from # PATH_INFO and reads page options such as h= and id= from QUERY_STRING, so # nginx must pass PATH_INFO through to the binary. cgit builds its own link # base from SCRIPT_NAME. The five static assets are served straight off disk # and must never be routed through cgit. # # nginx cannot run CGI programs itself, so a small bridge called fcgiwrap runs # the cgit.cgi binary and speaks FastCGI to nginx. Nothing below works until # that socket exists. On Debian and Ubuntu the packaged systemd socket # provides /run/fcgiwrap.socket, so enabling it is enough. # apt install fcgiwrap # systemctl enable --now fcgiwrap.socket # The socket must be readable by nginx's user. The packaged unit runs fcgiwrap # as www-data, which nginx also uses on those systems. Without systemd you can # run # spawn-fcgi -s /run/fcgiwrap.socket -M 660 -- /usr/sbin/fcgiwrap # or run fcgiwrap over TCP and point fastcgi_pass at 127.0.0.1:9000. # The user nginx drops to after binding the ports. It is www-data on Debian # and Ubuntu, nginx on RHEL and Fedora, and http on Arch and Alpine. It has to # match whatever owns the fcgiwrap socket. user www-data; worker_processes auto; pid /run/nginx.pid; # Startup and worker errors. Per-site request logs are set in the vhost. error_log /var/log/nginx/error.log warn; events { worker_connections 1024; } http { # nginx's built-in type table covers none of these five extensions, and # the usual "include mime.types" would pull in a second file, so the five # types are declared here. Everything else nginx returns comes from cgit, # which sets its own Content-Type. types { text/css css; text/javascript js; image/png png; image/vnd.microsoft.icon ico; text/plain txt; } default_type application/octet-stream; sendfile on; tcp_nopush on; keepalive_timeout 65; # Drop the version number from the Server header and from error pages. server_tokens off; # cgit pages are large and highly compressible, so this is the cheapest # speedup available. text/html is always compressed and cannot be listed. # Snapshot tarballs are deliberately absent, since they arrive compressed # already and running them through gzip again only burns CPU. gzip on; gzip_vary on; gzip_proxied any; gzip_min_length 1024; gzip_types text/css text/javascript text/plain application/atom+xml; # Requests carrying a Host header this config does not serve are dropped # without a response. cgit keys its page cache on the Host header, so # every invented hostname reaching it would mint a cache entry of its # own and evict a real page. 444 is nginx shorthand for closing the # connection without replying. server { listen 80 default_server; listen [::]:80 default_server; server_name _; return 444; } server { listen 443 ssl default_server; listen [::]:443 ssl default_server; server_name _; # Refuse the TLS handshake itself when the SNI name is unknown, which # also spares this block from needing a certificate. Requires nginx # 1.19.4 or newer. On older builds point ssl_certificate at any cert, # a self-signed one included, and rely on the return below. ssl_reject_handshake on; # A client can still handshake against a real name and then send some # other Host header. Those requests route here after the handshake, # past the rejection above, so close them too. return 444; } # Bounce plain HTTP up to HTTPS. Delete this whole server block if you # serve plain HTTP only. server { listen 80; listen [::]:80; server_name git.example.org; # ACME http-01 challenge files, if you use certbot in webroot mode. location ^~ /.well-known/acme-challenge/ { root /var/www/html; } # Everything else moves to HTTPS. location / { return 301 https://$host$request_uri; } } # The site itself, written for TLS on 443. For a quick plain-HTTP test, # change the two listen lines to port 80, delete the redirect block above, # and delete the http2, ssl and Strict-Transport-Security lines below. # Everything else stays as it is. server { listen 443 ssl; listen [::]:443 ssl; # nginx 1.25.1 and newer. On older builds delete this and write the # listen lines as "listen 443 ssl http2;" instead. http2 on; server_name git.example.org; ssl_certificate /etc/letsencrypt/live/git.example.org/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/git.example.org/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; # Let the client pick, which is the modern advice once the ancient # protocol versions are already excluded above. ssl_prefer_server_ciphers off; # Resumption, so a browser paging through a repository is not made to # redo a full handshake on every connection. ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; ssl_session_tickets off; # Site-wide security headers sit here because they must also cover the # static assets nginx serves directly. cgit itself sends only the # headers the proxy cannot supply. Those are Status, Content-Type, # Content-Length and Content-Disposition on downloads, Location on # redirects, a no-store Cache-Control on unauthenticated responses, the # auth filter's Set-Cookie, and on raw repository bytes a nosniff of its # own next to the stricter policy "default-src 'none'". Everything else, # this policy included, is the proxy's job. # # add_header appends and never replaces what cgit sent, so a raw page # carries both policies and the browser enforces the stricter one, # while the doubled nosniff line is harmless. Keep it that way. Any # construct that rewrites response headers here could strip the # protection cgit puts on raw repository content. # # form-action self covers the login form, the only form cgit # renders. always applies them to error responses too. If you enable # the gravatar or libravatar avatar filter, add its host to img-src, # for example https://www.gravatar.com or https://seccdn.libravatar.org. # A head-include or repo.head-content that injects a