aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTimo Wilken2023-11-05 01:03:55 +0100
committerTimo Wilken2023-11-05 01:03:55 +0100
commit7409fef3cbe6bba6c66ce8b03aef6c2d9dc6c7e7 (patch)
tree925bbc88193a26d0b0c5fb5b01842be614af4ea6
parentc3ef6ab1a62cf23cd15fd8865ec6bcdf8e7e4ad7 (diff)
Add secrets service
Allow managing secrets and passwords using Guix. Secrets are encrypted in the Guix channel repository and decrypted using a single host key at activation time.
-rw-r--r--tw/home.scm3
-rw-r--r--tw/services/secrets.scm169
-rw-r--r--tw/system/files/wireguard/lap-fp4.psk.enc1
-rw-r--r--tw/system/files/wireguard/lap-lud.psk.enc1
-rw-r--r--tw/system/files/wireguard/lap-pi3.psk.enc1
-rw-r--r--tw/system/files/wireguard/lap-vin.psk.enc1
-rw-r--r--tw/system/files/wireguard/lap.key.enc1
-rw-r--r--tw/system/lap.scm21
8 files changed, 198 insertions, 0 deletions
diff --git a/tw/home.scm b/tw/home.scm
index 17cb7da7..6b0402db 100644
--- a/tw/home.scm
+++ b/tw/home.scm
@@ -18,6 +18,8 @@
#:use-module (tw packages ci)
#:use-module (tw packages games)
#:use-module (tw packages xorg)
+ #:use-module ((tw services secrets)
+ #:select (encsecret-program))
#:use-module (tw system)
#:use-module (tw theme))
@@ -170,6 +172,7 @@
(simple-service 'common-files home-files-service-type
`((".infokey" ,(local-file "home/files/infokey"))
+ (".local/bin/encsecret" ,encsecret-program)
;; With #:recursive? #t, Guix keeps the files' permission bits, i.e. makes them executable.
(".local/bin/ppscm" ,(local-file "home/files/ppscm" #:recursive? #t)))) ; pretty-print scheme files
diff --git a/tw/services/secrets.scm b/tw/services/secrets.scm
new file mode 100644
index 00000000..41c26678
--- /dev/null
+++ b/tw/services/secrets.scm
@@ -0,0 +1,169 @@
+(define-module (tw services secrets)
+ #:use-module (gnu)
+ #:use-module (gnu packages guile-xyz)
+ #:use-module (gnu services)
+ #:use-module (gnu services configuration)
+ #:use-module (guix gexp)
+ #:use-module (guix modules)
+ #:use-module (guix packages)
+ #:use-module ((guix records) #:select (match-record))
+ #:use-module (srfi srfi-1)
+ #:export (secrets-service-type
+ secrets-configuration
+ secret
+ encsecret-program))
+
+(define-configuration/no-serialization secret
+ (encrypted-file file-like "The file in the Guix store containing the
+encrypted secret.")
+ (destination string "The file path into which the secret will be decrypted.")
+ (user (string "root") "The UNIX user owning the resulting decrypted file.")
+ (group (string "root") "The UNIX group owning the resulting decrypted file.")
+ (permissions (integer #o600) "UNIX file permissions for the resulting
+decrypted file. Accessible only by the file's owning user by default."))
+
+(define (list-of-secrets? thing)
+ (and (list? thing)
+ (every secret? thing)))
+
+(define-configuration/no-serialization secrets-configuration
+ (host-key (string "/etc/secrets.key") "The path to a file containing the
+decryption key for the given secrets.")
+ (secrets (list-of-secrets '()) "A list of @code{secret} records "))
+
+(define (secrets-activation config)
+ (match-record config <secrets-configuration> (host-key secrets)
+ (with-imported-modules (source-module-closure
+ '((guix base64)
+ (guix build utils)))
+ (with-extensions (list guile-sodium)
+ #~(begin
+ (use-modules (ice-9 format)
+ (ice-9 ports)
+ (ice-9 binary-ports)
+ (ice-9 textual-ports)
+ (rnrs bytevectors)
+ (srfi srfi-26)
+ (guix base64)
+ ((guix build utils) #:select (mkdir-p))
+ (sodium stream))
+ (define (install contents destination user group permissions)
+ (format (current-error-port) "Installing secret (~4,'0o ~a:~a) at ~a~%"
+ permissions user group destination)
+ (mkdir-p (dirname destination))
+ (let ((port (open-file destination "wb")))
+ (with-exception-handler
+ (lambda (exn)
+ (close port)
+ (raise-exception exn))
+ (lambda ()
+ ;; Change permissions before writing contents to avoid exposing
+ ;; the secret in the meantime.
+ (chown port (passwd:uid (getpw user)) (group:gid (getgr group)))
+ (chmod port permissions)
+ (put-bytevector port contents)
+ (close port)))))
+ ;; Generate a new host key if none exists yet.
+ ;; This allows instantiating this service with an empty list of
+ ;; secrets to generate a host key, and later add secrets.
+ (unless (file-exists? #$host-key)
+ (format (current-error-port)
+ "No host key found at ~a; creating one now~%" #$host-key)
+ (install (call-with-input-file "/dev/urandom"
+ (cut get-bytevector-n <> (crypto-stream-chacha20-ietf-keybytes)))
+ #$host-key "root" "root" #o600))
+ (define host-key
+ (call-with-input-file #$host-key get-bytevector-all #:binary #t))
+ (unless (= (crypto-stream-chacha20-ietf-keybytes)
+ (bytevector-length host-key))
+ (error "Invalid key detected; expected ~d bytes but got ~d bytes."
+ (crypto-stream-chacha20-ietf-keybytes)
+ (bytevector-length host-key)))
+ (define nonce
+ ;; In practice, `crypto-stream-chacha20-ietf-xor' is limited to
+ ;; 256 GiB of data for each (key, nonce) pair, but secrets are
+ ;; expected to be small, so it's fine to use the same nonce.
+ (make-bytevector (crypto-stream-chacha20-ietf-noncebytes) 0))
+ (define (decrypt encrypted-file)
+ (crypto-stream-chacha20-ietf-xor
+ #:message
+ (base64-decode
+ (string-delete ; `base64-decode' doesn't tolerate any whitespace
+ char-whitespace?
+ (call-with-input-file encrypted-file get-string-all)))
+ #:nonce nonce #:key host-key))
+ #$@(map (lambda (secret)
+ (match-record secret <secret> (encrypted-file destination user group permissions)
+ #~(install (decrypt #$encrypted-file) #$destination #$user #$group #$permissions)))
+ secrets))))))
+
+(define secrets-service-type
+ (service-type
+ (name 'secrets)
+ (extensions (list (service-extension activation-service-type secrets-activation)))
+ ;; `compose' is applied to unify all extensions into one first, ...
+ (compose concatenate)
+ ;; ...then `extend' combines the extensions with the initial config.
+ (extend (lambda (config more-secrets)
+ (secrets-configuration
+ (inherit config)
+ (secrets (append (secrets-configuration-secrets config)
+ more-secrets)))))
+ (default-value (secrets-configuration))
+ (description "Install files containing secrets on the system.")))
+
+;; The following can be installed e.g. in "~/.local/bin" and used to import
+;; secrets into local-files in a Guix channel.
+(define encsecret-program
+ (program-file "encsecret"
+ (with-imported-modules (source-module-closure
+ '((guix base64)))
+ (with-extensions (list guile-sodium)
+ #~(begin
+ (use-modules (ice-9 match)
+ (ice-9 ports)
+ (ice-9 binary-ports)
+ (rnrs bytevectors)
+ (guix base64)
+ (sodium stream))
+ (define (main key-port)
+ (let ((cryptotext
+ (crypto-stream-chacha20-ietf-xor
+ #:message (get-bytevector-all (current-input-port))
+ #:nonce (make-bytevector (crypto-stream-chacha20-ietf-noncebytes) 0)
+ #:key (get-bytevector-all key-port))))
+ (base64-encode cryptotext 0 (bytevector-length cryptotext)
+ #f #f base64-alphabet (current-output-port)))
+ (newline (current-output-port)))
+
+ (define (help-message program-name)
+ (string-append "\
+usage: " (basename program-name) " [-h] KEY_FILE
+
+This utility encrypts data passed on stdin to stdout using the given key file,
+in a way that the output can be decrypted with the same key file by the Guix
+secrets-service-type. Symmetric encryption is used. Processing this utility's
+base64-decoded output with the same key results in the original plaintext.
+
+arguments:
+ -h, --help show this message and exit
+ KEY_FILE the file containing the encryption key; required
+"))
+
+ (match (program-arguments)
+ ((program-name
+ . (? (lambda (args)
+ (or (member "-h" args)
+ (member "--help" args)))
+ _))
+ (display (help-message program-name)))
+
+ ((_ key-file)
+ (call-with-input-file key-file main #:binary #t))
+
+ ((program-name . _)
+ (display "error: invalid number of arguments\n\n"
+ (current-error-port))
+ (display (help-message program-name)
+ (current-error-port))
+ (exit 1))))))))
diff --git a/tw/system/files/wireguard/lap-fp4.psk.enc b/tw/system/files/wireguard/lap-fp4.psk.enc
new file mode 100644
index 00000000..ef979978
--- /dev/null
+++ b/tw/system/files/wireguard/lap-fp4.psk.enc
@@ -0,0 +1 @@
+ThP5USmpvehDO14PVZjqyYaBCy8n3jDIQoojYN95O98wvjOHq+Cs+t2NvDD3
diff --git a/tw/system/files/wireguard/lap-lud.psk.enc b/tw/system/files/wireguard/lap-lud.psk.enc
new file mode 100644
index 00000000..d976d90a
--- /dev/null
+++ b/tw/system/files/wireguard/lap-lud.psk.enc
@@ -0,0 +1 @@
+XTD6NjCM0JYnYU9+Z6q+9ZbpUBshnEPPMagwf/wCKMxBsXCXrt63oeqM1TD3
diff --git a/tw/system/files/wireguard/lap-pi3.psk.enc b/tw/system/files/wireguard/lap-pi3.psk.enc
new file mode 100644
index 00000000..2d318777
--- /dev/null
+++ b/tw/system/files/wireguard/lap-pi3.psk.enc
@@ -0,0 +1 @@
+XgTTJRaGle1fGipxe7i78Yr9HhUP3DiOQrctYMA6H/NcmzSV7uC/8d/XjjD3
diff --git a/tw/system/files/wireguard/lap-vin.psk.enc b/tw/system/files/wireguard/lap-vin.psk.enc
new file mode 100644
index 00000000..c84938a9
--- /dev/null
+++ b/tw/system/files/wireguard/lap-vin.psk.enc
@@ -0,0 +1 @@
+EQriKiyMh5x0Ak1WRqzg6q79DjcZ+jXNW50RTog5HqRMmSu3sNKf2seijjD3
diff --git a/tw/system/files/wireguard/lap.key.enc b/tw/system/files/wireguard/lap.key.enc
new file mode 100644
index 00000000..200bf821
--- /dev/null
+++ b/tw/system/files/wireguard/lap.key.enc
@@ -0,0 +1 @@
+ESqNLhK1ubNRY1JDVYuR1tGJDTUvnkKOTawBQcISO8Ntqgil59nRoOSNkjD3
diff --git a/tw/system/lap.scm b/tw/system/lap.scm
index 516de321..33154d5b 100644
--- a/tw/system/lap.scm
+++ b/tw/system/lap.scm
@@ -21,6 +21,7 @@
#:use-module (nonguix licenses)
#:use-module (tw channels)
#:use-module (tw packages scanner)
+ #:use-module (tw services secrets)
#:use-module (tw services wireguard)
#:use-module (tw system))
@@ -446,6 +447,26 @@ EndSection
(list vin)
(list lud))))))
+ (service secrets-service-type
+ (secrets-configuration
+ (secrets
+ (list
+ (secret
+ (encrypted-file (local-file "files/wireguard/lap.key.enc"))
+ (destination "/etc/wireguard/private.key"))
+ (secret
+ (encrypted-file (local-file "files/wireguard/lap-fp4.psk.enc"))
+ (destination "/etc/wireguard/fp4.psk"))
+ (secret
+ (encrypted-file (local-file "files/wireguard/lap-lud.psk.enc"))
+ (destination "/etc/wireguard/lud.psk"))
+ (secret
+ (encrypted-file (local-file "files/wireguard/lap-pi3.psk.enc"))
+ (destination "/etc/wireguard/pi3.psk"))
+ (secret
+ (encrypted-file (local-file "files/wireguard/lap-vin.psk.enc"))
+ (destination "/etc/wireguard/vin.psk"))))))
+
(modify-services (append %system-channel-services %desktop-services)
;; Let sane find the airscan backend. ipp-usb needs to be running separately.
(sane-service-type _ => sane-backends/airscan)