Subject: [bug#63375] [cuirass v3] doc: Document authentication.

From: Maxim Cournoyer <[email-masked]>
Date: Thu, 11 May 2023 00:34:52 -0400

* etc/new-client-cert.scm: Add script.
* doc/cuirass.texi (Authentication): Document it.
* Makefile.am (noinst_SCRIPTS): Register it.
---
 Makefile.am             |   2 +-
 doc/cuirass.texi        |  86 ++++++++++++++++++++++++++++
 etc/new-client-cert.scm | 121 ++++++++++++++++++++++++++++++++++++++++
 3 files changed, 208 insertions(+), 1 deletion(-)
 create mode 100755 etc/new-client-cert.scm

diff --git a/Makefile.am b/Makefile.am
index a40a76d..62b0860 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -25,7 +25,7 @@
 bin_SCRIPTS =                    \
   bin/cuirass
 
-noinst_SCRIPTS = pre-inst-env
+noinst_SCRIPTS = pre-inst-env etc/new-client-cert.scm
 
 guilesitedir = $(datarootdir)/guile/site/@GUILE_EFFECTIVE_VERSION@
 guileobjectdir = $(libdir)/guile/@GUILE_EFFECTIVE_VERSION@/site-ccache
diff --git a/doc/cuirass.texi b/doc/cuirass.texi
index db46a33..728ca7f 100644
--- a/doc/cuirass.texi
+++ b/doc/cuirass.texi
@@ -13,6 +13,7 @@ Copyright @copyright{} 2016, 2017 Mathieu Lirzin@*
 Copyright @copyright{} 2017, 2020, 2021 Mathieu Othacehe@*
 Copyright @copyright{} 2018, 2021 Ludovic Courtès@*
 Copyright @copyright{} 2018 Clément Lassieur
+Copyright @copyright{} 2023 Maxim Cournoyer@*
 
 @quotation
 Permission is granted to copy, distribute and/or modify this document
@@ -57,6 +58,7 @@ Documentation License''.
 * Parameters::                  Cuirass parameters.
 * Build modes::                 Build modes.
 * Invocation::                  How to run Cuirass.
+* Authentication::              Configuring TLS authentication.
 * Web API::                     Description of the Web API.
 * Database::                    About the database schema.
 
@@ -711,6 +713,90 @@ Display the actual version of @code{cuirass}.
 Display an help message that summarize all the options provided.
 @end table
 
+@c *********************************************************************
+@node Authentication
+@chapter Authentication
+@cindex authentication
+
+Cuirass does not provide its own authentication mechanism; by default,
+any user can do anything via its web interface.  To restrict this to
+only authorized users, one approach is to proxy the Cuirass web site via
+a web server such as Nginx and configure the web server to require
+client certificate verification for pages under the @samp{/admin}
+prefix.  The following minimal Nginx configuration can be used to
+accomplish this on a Guix System:
+
+@lisp
+(service nginx-service-type
+         (nginx-configuration
+          (server-blocks
+           (list
+            ;; TLS is required for authentication; serve the site via
+            ;; HTTPS only.
+            (nginx-server-configuration
+             (listen '("80"))
+             (raw-content
+              (list "return 308 https://$host$request_uri;")))
+
+            (nginx-server-configuration
+             (listen '("443 ssl"))
+             (server-name '("ci.your-host.org"))
+             (ssl-certificate "/etc/certs/ci.your-host.org.crt")
+             (ssl-certificate-key "/etc/certs/ci.your-host.org.key")
+             (locations
+              (list
+               ;; Proxy the whole Cuirass web site...
+               (nginx-location-configuration
+                (uri "/")
+                (body (list "proxy_pass http://localhost:8081;")))
+               ;; ... but require authentication for the admin pages.
+               (nginx-location-configuration
+                (uri "~ ^/admin")
+                (body
+                 (list "if ($ssl_client_verify != SUCCESS) \
+@{ return 403; @} proxy_pass http://localhost:8081;")))))
+             (raw-content
+              ;; Register your self-generated certificate authority.
+              (list "ssl_client_certificate /etc/ssl-ca/certs/ca.crt;"
+                    "ssl_verify_client optional;")))))))
+@end lisp
+
+Your host TLS certificate could have been obtained via Let's Encrypt or
+directly via the @command{openssl} command, among other means.  To
+create a private certificate authority (CA) that can sign user
+certificates, a convenience script is provided.  It's main requirement
+is to have the @command{guix} command available.  It can be invoked
+like:
+
+@example
+sudo -E ./etc/new-client-cert.scm --generate-ca
+@end example
+
+It should generate the @file{/etc/ssl-ca/private/ca.key} private key as
+well as the @file{/etc/ssl-ca/certs/ca.crt} certificate authority as
+used in the Nginx configuration above.
+
+To issue a new user certificate, run the same script from your home
+directory with:
+
+@example
+sudo -E ./etc/new-client-cert.scm
+@end example
+
+You will be asked to input the password for the CA private key, if any,
+and again for your new certificate; save it carefully.  The script
+requires to run as root to have access to the private certificate
+authority key; it outputs the new user certificate files to the current
+working directory.
+
+After your new CA-signed user certificate is generated, it needs to be
+registered with your web browser.  To do so using GNU IceCat, for
+example, you can navigate to @samp{Parameters -> Security -> Show
+certificates} and then click the @samp{Import...} button and select your
+@file{.pk12} personal certificate file.  The web interface of Cuirass
+should now only allow authenticated users to perform administrative
+tasks.
+
 @c *********************************************************************
 @node Web API
 @chapter Web API
diff --git a/etc/new-client-cert.scm b/etc/new-client-cert.scm
new file mode 100755
index 0000000..4fac772
--- /dev/null
+++ b/etc/new-client-cert.scm
@@ -0,0 +1,121 @@
+#!/usr/bin/env -S guix shell guile openssl -- guile \\
+--no-auto-compile -e main -s
+!#
+;;;; cuirass.scm -- Cuirass public interface.
+;;; Copyright © 2023 Ricardo Wurmus <[email-masked]>
+;;; Copyright © 2023 Maxim Cournoyer <[email-masked]>
+;;;
+;;; This file is part of Cuirass.
+;;;
+;;; Cuirass 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.
+;;;
+;;; Cuirass 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 Cuirass.  If not, see <http://www.gnu.org/licenses/>.
+
+(use-modules (ice-9 format)
+             (ice-9 match)
+             (guix build utils))
+
+(define %user (or (getenv "SUDO_USER")
+                  (getenv "USER")))
+
+(define %user-id (passwd:uid (getpwnam %user)))
+
+(define %group-id (passwd:gid (getpwnam %user)))
+
+(define %CA-directory
+  "/etc/ssl-ca")
+
+(define subject-template
+  "/C=DE/ST=Berlin/L=Berlin/O=GNU Guix/OU=Cuirass/CN=~a")
+
+(define CA-key
+  (string-append %CA-directory "/private/ca.key"))
+(define CA-cert
+  (string-append %CA-directory "/certs/ca.crt"))
+
+(define* (output who file)
+  (string-append (getcwd) "/" who file))
+
+(define (key-file who)
+  "Return the absolute file name of the key file for WHO."
+  (output who ".key"))
+
+(define (csr-file who)
+  "Return the absolute file name of the CSR file for WHO."
+  (output who ".csr"))
+
+(define (client-cert-file who)
+  "Return the absolute file name of the client certificate file for
+WHO."
+  (output who ".crt"))
+
+(define (exported-cert-file who)
+  "Return the absolute file name of the pkcs12 client certificate file
+for WHO.  This is the file that users should import into their
+browsers."
+  (output who ".p12"))
+
+(define (generate-ca!)
+  "Generate a private certificate authority (CA) valid for 10 years."
+  (mkdir-p (dirname CA-key))
+  (mkdir-p (dirname CA-cert))
+  (invoke "openssl" "req" "-newkey" "rsa" "-x509" "-days" "3650"
+	  "-noenc"                      ;no password
+	  "-subj" (format #false "~@?" subject-template "Cuirass CA")
+          "-keyout" CA-key "-out" CA-cert))
+
+(define (generate-csr! who)
+  "Generate a new certificate signing request and key for WHO."
+  (let ((key (key-file who))
+        (csr (csr-file who)))
+    (invoke "openssl" "req" "-newkey" "rsa"
+	    "-noenc"                    ;no password
+	    "-subj" (format #false "~@?" subject-template who)
+            "-keyout" key
+	    "-out" csr)
+    (chown key %user-id %group-id)
+    (chown csr %user-id %group-id)))
+
+(define* (generate-client-certificate! who #:key (expiry 365))
+  "Generate a client certificate for WHO."
+  (let ((cert (client-cert-file who)))
+    (invoke "openssl" "x509" "-req"
+            "-in" (csr-file who)
+            "-CA" CA-cert
+            "-CAkey" CA-key
+            "-out" cert
+            "-days" (number->string expiry))
+    (chown cert %user-id %group-id)))
+
+(define (export-p12! who)
+  (let ((key (key-file who))
+        (exported-cert (exported-cert-file who)))
+    (invoke "openssl" "pkcs12" "-export"
+	    "-in" (client-cert-file who)
+	    "-inkey" key
+	    "-out" exported-cert)
+    (chown key %user-id %group-id)
+    (chown exported-cert %user-id %group-id)))
+
+(define (main args)
+  (match (command-line)
+    ((script)
+     (set-program-arguments (list script %user))
+     (apply main args))
+    ((script "--generate-ca")
+     (generate-ca!))
+    ((script who)
+     (generate-csr! who)
+     (generate-client-certificate! who)
+     (export-p12! who))
+    ((script . rest)
+     (format (current-error-port) "usage: ~a [--generate-ca|name]~%" script))))

base-commit: cf4e3e4ac4a9c8d6f0d82b0a173826f15bbca7f3
-- 
2.39.2