# 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. Passing PATH_INFO through is the # single most important part of the config below. # # 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 compiled-in type table knows only text/html, and the usual # "include mime.types" would pull in a second file. Exactly five static # files are served off disk, so their types are declared here instead and # this config keeps standing on its own. 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, a raw IP or # an invented name from a scanning bot, land in these two blocks and are # dropped without a response. cgit keys its page cache on the Host header # so clone URLs stay honest, which means every invented hostname reaching # it would mint a cache entry of its own and evict a real page to make # room. Refusing strangers here keeps the cache to the names the site # actually answers to. 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, 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. # # cgit loads only its own /cgit.js and uses inline style on the # diffstat bars, so script-src stays self while style-src allows # inline. 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. add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; base-uri 'none'; frame-ancestors 'self'; form-action 'self'" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "no-referrer" always; # The browser features a git viewer never asks for, camera and # location among them, are refused outright for everything served # here, repository files included. add_header Permissions-Policy "accelerometer=(), autoplay=(), camera=(), display-capture=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), usb=()" always; # Cross-origin isolation. The opener policy cuts any window.opener # link between cgit and pages that open it, and the resource policy # stops other origins from embedding what cgit serves, a hotlinked # raw file for example. git clients are not browsers and ignore both, # so clone and snapshot downloads keep working. The stricter # Cross-Origin-Embedder-Policy is deliberately absent because it # would break the avatar filters mentioned above. add_header Cross-Origin-Opener-Policy "same-origin" always; add_header Cross-Origin-Resource-Policy "same-origin" always; # Two years of forced HTTPS. This config already redirects every # plain request to TLS, so browsers may as well stop asking. Delete # this line if you convert the vhost to plain HTTP, and know it is # hard to undo once browsers have seen it. add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; # The document root is the directory that holds the static assets. cgit # emits absolute links to /cgit.css and /cgit.png by default, so those # files must resolve at the root of the URL space. Pointing root at the # asset directory makes /cgit.css map to /usr/share/cgit/cgit.css. root /usr/share/cgit; # The only request body cgit ever reads is the auth-filter login form, # and it stops after 4096 bytes. Nothing else here accepts an upload, # so keep the cap far below the nginx default of 1m. client_max_body_size 64k; access_log /var/log/nginx/cgit.access.log combined; error_log /var/log/nginx/cgit.error.log; # Match the assets by their exact root-level names, never by bare # extension. cgit routes on PATH_INFO and a repository can hold files # ending in .css or .png, so /myrepo/tree/style.css and /myrepo/plain/ # logo.png are real cgit URLs. A broad extension match would capture # those, look for them on disk, and return 404 before cgit could render # them. Anchoring the regex at the start of the path matches /cgit.css # but not /myrepo/tree/cgit.css, so it can never shadow a repository # file. An nginx regex location is matched before the prefix location # below, so these assets win for their exact URLs and cgit wins for the # rest. location ~ ^/(cgit\.css|cgit\.js|cgit\.png|favicon\.ico|robots\.txt)$ { expires 30d; access_log off; try_files $uri =404; } # Everything that is not a static asset above is a cgit URL, the repo # index, a repository, a page within a repository, a snapshot, a feed. location / { # The CGI environment. This is normally "include fastcgi_params", # which would be a second file, so the list is written out here. # Of all of it cgit itself reads only CGIT_CONFIG, PATH_INFO, # QUERY_STRING, SCRIPT_NAME, REQUEST_METHOD, CONTENT_LENGTH, # HTTP_HOST, HTTPS, SERVER_NAME, SERVER_PORT, HTTP_COOKIE and # HTTP_REFERER. The cookie and referer arrive on their own, since # nginx forwards request headers as HTTP_* without being asked. # The program fcgiwrap runs. It must be the cgit binary itself, not # $document_root$fastcgi_script_name, which would try to run a repo # path and is the usual cause of a failed request. fastcgi_param SCRIPT_FILENAME /usr/lib/cgit/cgit.cgi; # cgit builds its link base, the virtual root, from SCRIPT_NAME. # The stock parameters set SCRIPT_NAME to the whole request path, # which would make cgit prepend that path to every link. Served at # the domain root the script has no prefix, so force SCRIPT_NAME # empty and cgit uses / as its base. A sub-path install sets it # instead, see the end of this file. fastcgi_param SCRIPT_NAME ""; # How cgit learns the repository and page. At the domain root the # whole request path is the PATH_INFO. fastcgi_param PATH_INFO $uri; # Page options such as h=branch, id=sha and the snapshot format. fastcgi_param QUERY_STRING $query_string; # Which config cgit reads. It checks CGIT_CONFIG and falls back to # the compiled-in /etc/cgitrc. Setting it makes the location # explicit and lets you move cgitrc without recompiling. fastcgi_param CGIT_CONFIG /etc/cgitrc; # The browser's Host header, so cgit builds clone URLs against the # name the visitor used rather than server_name. fastcgi_param HTTP_HOST $http_host; # The real scheme, so cgit builds correct https clone URLs. fastcgi_param HTTPS $https if_not_empty; # The routine remainder, needed by fcgiwrap and by the login form. fastcgi_param REQUEST_METHOD $request_method; fastcgi_param CONTENT_TYPE $content_type; fastcgi_param CONTENT_LENGTH $content_length; fastcgi_param REQUEST_URI $request_uri; fastcgi_param DOCUMENT_URI $document_uri; fastcgi_param DOCUMENT_ROOT $document_root; fastcgi_param SERVER_PROTOCOL $server_protocol; fastcgi_param REQUEST_SCHEME $scheme; fastcgi_param GATEWAY_INTERFACE CGI/1.1; fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; fastcgi_param REMOTE_ADDR $remote_addr; fastcgi_param REMOTE_PORT $remote_port; fastcgi_param SERVER_ADDR $server_addr; fastcgi_param SERVER_PORT $server_port; fastcgi_param SERVER_NAME $server_name; # Hand off to the fcgiwrap socket. A TCP fcgiwrap would use for # example 127.0.0.1:9000 here. fastcgi_pass unix:/run/fcgiwrap.socket; # Large outputs such as snapshot tarballs and blame on big files # can take a while, so give cgit room and stream rather than buffer. fastcgi_read_timeout 300s; fastcgi_buffering off; # cgit sends no caching headers of its own, so browsers refetch # dynamic pages and cgit's internal cache keeps that cheap. Do not # add a blanket expires or Cache-Control in this location. It # would fight the no-store cgit puts on the login page and could # let one visitor's page be served to another from a shared cache. } } } # To serve cgit at https://git.example.org/cgit/ instead of the root, split # the URL so SCRIPT_NAME is the prefix and PATH_INFO is the rest. Keep every # other fastcgi_param from the location above. # # location /cgit/ { # fastcgi_split_path_info ^(/cgit)(/.*)$; # fastcgi_param SCRIPT_FILENAME /usr/lib/cgit/cgit.cgi; # fastcgi_param SCRIPT_NAME $fastcgi_script_name; # fastcgi_param PATH_INFO $fastcgi_path_info; # ... # fastcgi_pass unix:/run/fcgiwrap.socket; # } # # Serve the assets from the sub-path too, again anchored to the exact names. # # location ~ ^/cgit/(cgit\.css|cgit\.js|cgit\.png|favicon\.ico|robots\.txt)$ { # alias /usr/share/cgit/$1; # expires 30d; # access_log off; # } # # cgit's default css=/cgit.css and logo=/cgit.png point at the domain root, so # under a sub-path also set css=/cgit/cgit.css and logo=/cgit/cgit.png in # cgitrc. cgit derives the /cgit prefix from SCRIPT_NAME. If links come out # wrong, pin it in cgitrc with virtual-root=/cgit/.