diff options
Diffstat (limited to 'guix/forge')
| -rw-r--r-- | guix/forge/anubis.scm | 142 | ||||
| -rw-r--r-- | guix/forge/cgit.scm | 145 | ||||
| -rw-r--r-- | guix/forge/klaus.scm | 9 | ||||
| -rw-r--r-- | guix/forge/nginx.scm | 201 |
4 files changed, 385 insertions, 112 deletions
diff --git a/guix/forge/anubis.scm b/guix/forge/anubis.scm new file mode 100644 index 0000000..63f7a54 --- /dev/null +++ b/guix/forge/anubis.scm @@ -0,0 +1,142 @@ +;;; guix-forge --- Guix software forge meta-service +;;; Copyright © 2026 Ashish Shukla <ashish.is@lostca.se> +;;; Copyright © 2026 Arun Isaac <arunisaac@systemreboot.net> +;;; +;;; This file is part of guix-forge. +;;; +;;; guix-forge is free software: you can redistribute it and/or modify +;;; it under the terms of the GNU General Public License as published +;;; by the Free Software Foundation, either version 3 of the License, +;;; or (at your option) any later version. +;;; +;;; guix-forge is distributed in the hope that it will be useful, but +;;; WITHOUT ANY WARRANTY; without even the implied warranty of +;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +;;; General Public License for more details. +;;; +;;; You should have received a copy of the GNU General Public License +;;; along with guix-forge. If not, see +;;; <https://www.gnu.org/licenses/>. + +(define-module (forge anubis) + #:use-module ((gnu packages golang) #:select (go-1.26)) + #:use-module (gnu services) + #:use-module (gnu services shepherd) + #:use-module (gnu system file-systems) + #:use-module (guix build-system gnu) + #:use-module (guix download) + #:use-module (guix gexp) + #:use-module (guix least-authority) + #:use-module ((guix licenses) #:prefix license:) + #:use-module (guix packages) + #:use-module (guix records) + #:use-module (ice-9 match) + #:export (anubis-configuration + anubis-configuration? + anubis-configuration-package + anubis-service-type + %anubis-unix-socket)) + +;; TODO: Unbundle vendored node and go dependencies. See work in progress at +;; https://codeberg.org/guix/guix/pulls/2572 +;; This package is based on https://codeberg.org/group/guix-modules/src/commit/137fe9d6dcdad1582c64a70c6f8a052c9251a590/guix/abbe/packages/golang.scm#L902 +(define-public anubis-ai-firewall + (package + (name "anubis-ai-firewall") + (version "1.26.2") + (source + (origin + (method url-fetch) + (uri (string-append "https://github.com/TecharoHQ/anubis/releases/download/v" + version "/anubis-src-vendor-npm-" version ".tar.gz")) + (sha256 + (base32 "1yab3z58vgi16313wmx7g32xk6nv158lqic54qds43l63y6lmf92")))) + (build-system gnu-build-system) + (arguments + (list #:phases + #~(modify-phases %standard-phases + (delete 'configure) + (add-after 'unpack 'patch-Makefile + (lambda _ + (substitute* "Makefile" + (("\\(GO\\) build" all) + (string-append all " -trimpath"))))) + (replace 'build + (lambda _ + (let ((tmpdir "/tmp")) + (setenv "TMPDIR" tmpdir) + (setenv "GOPATH" (string-append tmpdir "/go")) + (setenv "GOCACHE" (string-append tmpdir "/go-cache")) + (invoke "make" "prebaked-build")))) + (replace 'check + (lambda* (#:key tests? #:allow-other-keys) + (when tests? + (invoke "go" "test" "./..." + ;; This test requires network access. + "-skip" "TestLookup")))) + (replace 'install + (lambda _ + (install-file "var/anubis" + (string-append #$output "/bin"))))))) + (native-inputs + (list go-1.26)) + (home-page "https://anubis.techaro.lol/") + (synopsis "Identify and block HTTP requests from AI bots") + (description + "Anubis is a web AI firewall utility that uses a combination of heuristics +and challenges to identify and block bots before they take your website down. +Anubis is as lightweight as possible and is designed to help protect the small +internet from the endless storm of requests that flood in from AI companies.") + (license license:expat))) + +(define-record-type* <anubis-configuration> + anubis-configuration make-anubis-configuration + anubis-configuration? + (package anubis-configuration-package + (default anubis-ai-firewall))) + +;; TODO: Do not hard-code. +(define %anubis-unix-socket + "/var/run/anubis/socket") + +(define (anubis-activation config) + #~(begin + (let ((user (getpw "nginx"))) + (mkdir-p (dirname #$%anubis-unix-socket)) + (chown (dirname #$%anubis-unix-socket) + (passwd:uid user) + (passwd:gid user))))) + +(define anubis-shepherd-service + (match-lambda + (($ <anubis-configuration> package) + (shepherd-service + (documentation "Run the Anubis AI firewall.") + (provision '(anubis)) + (requirement '(networking)) + (start #~(make-forkexec-constructor + (list #$(least-authority-wrapper + (file-append package "/bin/anubis") + #:name "anubis-pola-wrapper" + #:mappings (list (file-system-mapping + (source (dirname %anubis-unix-socket)) + (target source) + (writable? #t)))) + "-bind" #$%anubis-unix-socket + "-bind-network" "unix" + "-target" " ") + #:user "nginx" + #:group "nginx" + #:log-file "/var/log/anubis.log")) + (stop #~(make-kill-destructor)))))) + +(define anubis-service-type + (service-type + (name 'anubis) + (description "Run the Anubis AI firewall.") + (extensions + (list (service-extension activation-service-type + anubis-activation) + (service-extension shepherd-root-service-type + (compose list anubis-shepherd-service)))) + (default-value (anubis-configuration)))) diff --git a/guix/forge/cgit.scm b/guix/forge/cgit.scm index 0956eb6..a607f5f 100644 --- a/guix/forge/cgit.scm +++ b/guix/forge/cgit.scm @@ -25,13 +25,18 @@ #:use-module (forge socket) #:use-module ((gnu packages emacs) #:select (emacs-minimal)) #:use-module ((gnu packages mail) #:select (mailcap)) + #:use-module ((gnu packages golang-apps) #:select (go-chroma)) #:use-module ((gnu packages version-control) #:select (cgit-pink git-minimal)) #:use-module (gnu services) #:use-module ((gnu services web) #:select (nginx-server-configuration - nginx-location-configuration)) + nginx-location-configuration + nginx-named-location-configuration + nginx-named-location-configuration-body)) #:use-module (gnu system file-systems) + #:use-module (guix diagnostics) #:use-module (guix gexp) + #:use-module (guix i18n) #:use-module (guix records) #:use-module (ice-9 match) #:use-module (srfi srfi-26) @@ -68,6 +73,21 @@ "README.txt" "README"))) +(define %cgit-chroma-syntax-highlight-gexp + (with-imported-modules '((guix build utils)) + #~(begin + (use-modules (guix build utils) + (ice-9 match)) + + (match (command-line) + ((_ file) + (invoke #$(file-append go-chroma "/bin/chroma") + "--style" "pastie" + "--html" + "--html-only" + "--html-inline-styles" + "--filename" file)))))) + (define-record-type* <cgit-configuration> cgit-configuration make-cgit-configuration cgit-configuration? @@ -91,15 +111,18 @@ (about-filter cgit-configuration-about-filter (default (program-file "about-filter" (about-filter-gexp this-cgit-configuration))) + ;; thunked so that value can reference the cgit package in + ;; configuration (thunked)) (commit-filter cgit-configuration-commit-filter (default #f)) (email-filter cgit-configuration-email-filter (default #f)) (source-filter cgit-configuration-source-filter - (default (file-append (cgit-configuration-cgit - this-cgit-configuration) - "/lib/cgit/filters/syntax-highlighting.py")) + (default (program-file "cgit-chroma-syntax-highlight" + %cgit-chroma-syntax-highlight-gexp)) + ;; thunked so that value can reference the cgit package in + ;; configuration (thunked)) (mimetype-file cgit-configuration-mimetype-file (default (file-append mailcap "/etc/mime.types"))) @@ -118,9 +141,9 @@ "tar.xz" "tar.zst" "zip"))) (for-each (lambda (snapshot) (unless (member snapshot valid-snapshots) - (leave (G_ "Snapshot ~a is not one of ~s.~%" - snapshot - valid-snapshots)))) + (leave (G_ "Snapshot ~a is not one of ~s.~%") + snapshot + valid-snapshots))) snapshots) snapshots)) @@ -236,63 +259,77 @@ configured in @var{config}." (name "CGIT_CONFIG") (value cgitrc)))) (mappings (list (file-system-mapping - (source repository-directory) - (target source)) + (source repository-directory) + (target source)) (file-system-mapping - (source cgit) - (target source)) + (source cgit) + (target source)) (file-system-mapping - (source cgitrc) - (target source)) + (source cgitrc) + (target source)) (file-system-mapping - (source (file-append git "/libexec/git-core/git-http-backend")) - (target source)))))))) + (source (file-append git "/libexec/git-core/git-http-backend")) + (target source)))))))) (define cgit-nginx-server-block (match-record-lambda <cgit-configuration> - (cgit git server-name socket repository-directory) - (nginx-server-configuration + (cgit git server-name socket repository-directory) + (forge-nginx-server-configuration (server-name (list server-name)) ;; cgit static files (root (file-append cgit "/share/cgit")) - (try-files (list "$uri" "@cgit")) + (anubis? #t) (locations - (list - ;; git-http-backend for the smart HTTP protocol - (nginx-location-configuration - (uri "~ ^/.*/(HEAD|info/refs|git-receive-pack|git-upload-pack).*$") - (body (list "fastcgi_param SCRIPT_FILENAME " - (file-append git "/libexec/git-core/git-http-backend;") - (string-append "fastcgi_param GIT_PROJECT_ROOT " - repository-directory - ";") - "fastcgi_param GIT_HTTP_EXPORT_ALL yes;" - "fastcgi_param PATH_INFO $uri;" - "fastcgi_param QUERY_STRING $query_string;" - "fastcgi_param REQUEST_METHOD $request_method;" - "fastcgi_param CONTENT_TYPE $content_type;" - (string-append "fastcgi_pass " - (nginx-socket->string socket) - ";")))) - ;; cgit web interface - (nginx-location-configuration - (uri "@cgit") - (body (list - #~(string-append "fastcgi_param SCRIPT_FILENAME " - #$(file-append cgit "/lib/cgit/cgit.cgi") - ";") - "fastcgi_param PATH_INFO $uri;" - "fastcgi_param QUERY_STRING $query_string;" - "fastcgi_param HTTP_HOST $server_name;" - (string-append "fastcgi_pass " - (nginx-socket->string socket) - ";"))))))))) + (let ((cgit-location + (nginx-named-location-configuration + (name "cgit") + (body (list + #~(string-append "fastcgi_param SCRIPT_FILENAME " + #$(file-append cgit "/lib/cgit/cgit.cgi") + ";") + "fastcgi_param PATH_INFO $uri;" + "fastcgi_param QUERY_STRING $query_string;" + "fastcgi_param HTTP_HOST $server_name;" + (string-append "fastcgi_pass " + (nginx-socket->string socket) + ";")))))) + (list + ;; git-http-backend for the smart HTTP protocol + (nginx-location-configuration + (uri "~ ^/.*/(HEAD|info/refs|git-receive-pack|git-upload-pack).*$") + (body (list "fastcgi_param SCRIPT_FILENAME " + (file-append git "/libexec/git-core/git-http-backend;") + (string-append "fastcgi_param GIT_PROJECT_ROOT " + repository-directory + ";") + "fastcgi_param GIT_HTTP_EXPORT_ALL yes;" + "fastcgi_param PATH_INFO $uri;" + "fastcgi_param QUERY_STRING $query_string;" + "fastcgi_param REQUEST_METHOD $request_method;" + "fastcgi_param CONTENT_TYPE $content_type;" + (string-append "fastcgi_pass " + (nginx-socket->string socket) + ";")))) + ;; cgit web interface + cgit-location + ;; Protect the commit, diff and tree URIs behind Anubis. + (nginx-location-configuration + (uri "~ ^/.*/(diff|tree)/") + (body (cons* "auth_request /.within.website/x/cmd/anubis/api/check;" + "error_page 401 = @redirectToAnubis;" + (nginx-named-location-configuration-body cgit-location)))) + ;; On other URIs, try a file before falling back to @cgit. This + ;; try_files cannot be in the server context because it would then + ;; apply to git-http-backend, etc. + (nginx-location-configuration + (uri "/") + (body (list "try_files $uri @cgit;"))))))))) (define cgit-service-type (service-type - (name 'cgit) - (description "Run cgit.") - (extensions (list (service-extension fcgiwrap-service-type - (compose list cgit-fcgiwrap-instance)) - (service-extension forge-nginx-service-type - (compose list cgit-nginx-server-block)))))) + (name 'cgit) + (description "Run cgit.") + (extensions (list (service-extension fcgiwrap-service-type + (compose list cgit-fcgiwrap-instance)) + (service-extension forge-nginx-service-type + (compose list cgit-nginx-server-block)))))) diff --git a/guix/forge/klaus.scm b/guix/forge/klaus.scm index 11cdbd5..157a974 100644 --- a/guix/forge/klaus.scm +++ b/guix/forge/klaus.scm @@ -34,6 +34,7 @@ nginx-location-configuration)) #:use-module (gnu system file-systems) #:use-module (guix deprecation) + #:use-module (guix gexp) #:use-module (guix packages) #:use-module (guix records) #:export (<klaus-configuration> @@ -44,6 +45,7 @@ klaus-configuration-socket klaus-configuration-repository-directory klaus-configuration-site-name + klaus-configuration-gunicorn-workers klaus-service-type klaus-gunicorn-app)) @@ -74,16 +76,19 @@ (repository-directory klaus-configuration-repository-directory (default "/srv/git")) (site-name klaus-configuration-site-name - (default #f))) + (default #f)) + (gunicorn-workers klaus-configuration-gunicorn-workers + (default #~(* 2 (total-processor-count))))) (define klaus-gunicorn-apps (match-record-lambda <klaus-configuration> - (python-klaus socket repository-directory site-name) + (python-klaus socket repository-directory site-name gunicorn-workers) (list (gunicorn-app (name "klaus") (package python-klaus) (wsgi-app-module "klaus.contrib.wsgi_autoreload") (sockets (list socket)) + (workers gunicorn-workers) (environment-variables (cons* (environment-variable (name "KLAUS_REPOS_ROOT") (value repository-directory)) diff --git a/guix/forge/nginx.scm b/guix/forge/nginx.scm index 69bcb31..87ad222 100644 --- a/guix/forge/nginx.scm +++ b/guix/forge/nginx.scm @@ -19,12 +19,16 @@ (define-module (forge nginx) #:use-module (forge acme) + #:use-module (forge anubis) #:use-module (forge socket) #:use-module ((gnu packages admin) #:select (shepherd)) + #:use-module ((gnu packages web) #:select (nginx)) #:use-module (gnu services) #:use-module (gnu services web) + #:use-module (guix packages) #:use-module (guix gexp) #:use-module (guix records) + #:use-module (guix utils) #:use-module (ice-9 match) #:use-module (srfi srfi-1) #:export (<forge-nginx-configuration> @@ -36,10 +40,31 @@ forge-nginx-configuration-acme-state-directory forge-nginx-configuration-acme-challenge-directory forge-nginx-configuration-server-blocks + + <forge-nginx-server-configuration> + forge-nginx-server-configuration + forge-nginx-server-configuration? + forge-nginx-server-configuration-server-name + forge-nginx-server-configuration-root + forge-nginx-server-configuration-locations + forge-nginx-server-configuration-index + forge-nginx-server-configuration-try-files + forge-nginx-server-configuration-anubis? + forge-nginx-server-configuration-raw-content + nginx-socket->string socket->nginx-proxy-pass forge-nginx-service-type)) +(define-public nginx-with-auth-request + (package + (inherit nginx) + (arguments + (substitute-keyword-arguments (package-arguments nginx) + ((#:configure-flags flags '()) + #~(cons "--with-http_auth_request_module" + #$flags)))))) + (define-record-type* <forge-nginx-configuration> forge-nginx-configuration make-forge-nginx-configuration forge-nginx-configuration? @@ -60,6 +85,24 @@ (server-blocks forge-nginx-configuration-server-blocks (default '()))) +(define-record-type* <forge-nginx-server-configuration> + forge-nginx-server-configuration make-forge-nginx-server-configuration + forge-nginx-server-configuration? + (server-name forge-nginx-server-configuration-server-name + (default (list 'default))) + (root forge-nginx-server-configuration-root + (default "/srv/http")) + (locations forge-nginx-server-configuration-locations + (default '())) + (index forge-nginx-server-configuration-index + (default (list "index.html"))) + (try-files forge-nginx-server-configuration-try-files + (default '())) + (anubis? forge-nginx-server-configuration-anubis? + (default #f)) + (raw-content forge-nginx-server-configuration-raw-content + (default '()))) + (define (nginx-socket->string socket) "Serialize @var{socket} to a string as expected by nginx configuration (for example, in the @code{listen} and @@ -92,48 +135,94 @@ directive." (string-append "http://unix:" path ":"))) ";")) +(define (forge-nginx-configuration-coerced-server-blocks config) + "Return server blocks in @code{forge-nginx-configuration} @var{config}, all +coerced into @code{<forge-nginx-server-configuration>} objects." + (map (lambda (server) + (if (nginx-server-configuration? server) + (forge-nginx-server-configuration + (server-name (nginx-server-configuration-server-name server)) + (root (nginx-server-configuration-root server)) + (locations (nginx-server-configuration-locations server)) + (index (nginx-server-configuration-index server)) + (try-files (nginx-server-configuration-try-files server)) + (raw-content (nginx-server-configuration-raw-content server))) + server)) + (forge-nginx-configuration-server-blocks config))) + +(define (anubis-locations anubis-socket) + ;; This configuration is based on + ;; https://anubis.techaro.lol/docs/admin/configuration/subrequest-auth + (list (nginx-location-configuration + ;; Reverse proxy to Anubis for authorization and challenges. This + ;; location cannot be internal because clients need to request + ;; challenge pages. + (uri "/.within.website/") + (body (list (socket->nginx-proxy-pass anubis-socket) + "proxy_set_header X-Real-IP $remote_addr;" + "proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;" + "proxy_set_header Host $http_host;" + "proxy_pass_request_body off;" + "proxy_set_header Content-Length \"\";"))) + ;; Redirect to Anubis challenge if authorization failed. We may be + ;; listening locally on a non-standard port; do not expose that to the + ;; end user. + (nginx-named-location-configuration + (name "redirectToAnubis") + (body (list "port_in_redirect off;" + "return 307 /.within.website/?redir=$scheme://$host$request_uri;"))))) + (define (forge-nginx-server-blocks config) "Return list of nginx server blocks to provision for forge-web service specified by @var{config}." (match-record config <forge-nginx-configuration> - (http-listen https-listen proxy-protocol-listen acme-state-directory acme-challenge-directory server-blocks) + (http-listen https-listen proxy-protocol-listen acme-state-directory acme-challenge-directory) (cons (nginx-server-configuration - (listen (list (nginx-socket->string http-listen))) - (locations - (list (nginx-location-configuration - (uri "/.well-known/acme-challenge/") - ;; Without a trailing slash, an alias of /var/foo - ;; would lookup /bar at /var/foobar, not - ;; /var/foo/bar. So, a trailing slash is - ;; significant. Append it if not already - ;; present. Likewise, the trailing slash in - ;; "/.well-known/acme-challenge/" is also - ;; significant. - (body (list (string-append "alias " - (string-trim-right acme-challenge-directory #\/) - "/;")))) - (nginx-location-configuration - (uri "/") - ;; HTTP Strict Transport Security (HSTS) header as - ;; recommended by https://hstspreload.org - (body (list "add_header Strict-Transport-Security \"max-age=63072000; includeSubdomains; preload\";" - "return 301 https://$host$request_uri;")))))) - (map (lambda (server) - (match (nginx-server-configuration-server-name server) + (listen (list (nginx-socket->string http-listen))) + (locations + (list (nginx-location-configuration + (uri "/.well-known/acme-challenge/") + ;; Without a trailing slash, an alias of /var/foo would + ;; lookup /bar at /var/foobar, not /var/foo/bar. So, a + ;; trailing slash is significant. Append it if not already + ;; present. Likewise, the trailing slash in + ;; "/.well-known/acme-challenge/" is also significant. + (body (list (string-append "alias " + (string-trim-right acme-challenge-directory #\/) + "/;")))) + (nginx-location-configuration + (uri "/") + ;; HTTP Strict Transport Security (HSTS) header as + ;; recommended by https://hstspreload.org + (body (list "add_header Strict-Transport-Security \"max-age=63072000; includeSubdomains; preload\";" + "return 301 https://$host$request_uri;")))))) + (map (match-record-lambda <forge-nginx-server-configuration> + (server-name root locations index try-files anubis? raw-content) + (match server-name ((name _ ...) (nginx-server-configuration - (inherit server) - (listen (cons (string-append (nginx-socket->string https-listen) - " ssl") - (if proxy-protocol-listen - (list (string-append (nginx-socket->string proxy-protocol-listen) - " ssl proxy_protocol")) - (list)))) - (ssl-certificate (string-append acme-state-directory - "/" name "/cert.pem")) - (ssl-certificate-key (string-append acme-state-directory - "/private/" name "/key.pem")))))) - server-blocks)))) + (listen (cons (string-append (nginx-socket->string https-listen) + " ssl") + (if proxy-protocol-listen + (list (string-append (nginx-socket->string proxy-protocol-listen) + " ssl proxy_protocol")) + (list)))) + (server-name server-name) + (root root) + (locations + (if anubis? + (append (anubis-locations (forge-unix-socket + (path %anubis-unix-socket))) + locations) + locations)) + (index index) + (try-files try-files) + (ssl-certificate (string-append acme-state-directory + "/" name "/cert.pem")) + (ssl-certificate-key (string-append acme-state-directory + "/private/" name "/key.pem")) + (raw-content raw-content))))) + (forge-nginx-configuration-coerced-server-blocks config))))) (define %deploy-hook-gexp (with-imported-modules '((guix build utils)) @@ -151,27 +240,27 @@ specified by @var{config}." (define (forge-nginx-acme-certificates config) "Return list of @code{<acme-certificate>} blocks to provision for forge-nginx service specified by @var{config}." - (match-record config <forge-nginx-configuration> - (server-blocks) - (map (lambda (server) - (acme-certificate - (domains (nginx-server-configuration-server-name server)) - (deploy-hook (program-file "forge-nginx-acme-deploy-hook" - %deploy-hook-gexp)))) - server-blocks))) + (map (lambda (server) + (acme-certificate + (domains (forge-nginx-server-configuration-server-name server)) + (deploy-hook (program-file "forge-nginx-acme-deploy-hook" + %deploy-hook-gexp)))) + (forge-nginx-configuration-coerced-server-blocks config))) (define forge-nginx-service-type (service-type - (name 'forge-nginx) - (description "Run the forge-nginx web server.") - (extensions (list (service-extension nginx-service-type - forge-nginx-server-blocks) - (service-extension acme-service-type - forge-nginx-acme-certificates))) - (compose concatenate) - (extend (lambda (config server-blocks) - (forge-nginx-configuration - (inherit config) - (server-blocks (append (forge-nginx-configuration-server-blocks config) - server-blocks))))) - (default-value (forge-nginx-configuration)))) + (name 'forge-nginx) + (description "Run the forge-nginx web server.") + (extensions (list (service-extension nginx-service-type + forge-nginx-server-blocks) + (service-extension acme-service-type + forge-nginx-acme-certificates) + (service-extension anubis-service-type + (const #t)))) + (compose concatenate) + (extend (lambda (config server-blocks) + (forge-nginx-configuration + (inherit config) + (server-blocks (append (forge-nginx-configuration-server-blocks config) + server-blocks))))) + (default-value (forge-nginx-configuration)))) |
