summaryrefslogtreecommitdiff
path: root/tw/services/secrets.scm
blob: 1895700dd2353c9a471be555ffd328efd7e1decd (about) (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
(define-module (tw services secrets)
  #:use-module (gnu)
  #:use-module (gnu packages golang)
  #: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.  The file must have been encrypted to the @code{host-key}
specified in the host's @code{secrets-configuration} record.")
  (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/ssh/ssh_host_ed25519_key") "The path to a file
containing the decryption key for the given secrets.")
  (secrets (list-of-secrets '()) "A list of @code{secret} records to
install on the host."))

(define (secrets-activation config)
  (define (secret-install-invocation secret)
    (match-record secret <secret> (encrypted-file destination user group permissions)
      ;; Call the `install' function defined in the gexp below.
      #~(install #$encrypted-file #$destination #$user #$group #$permissions)))

  (match-record config <secrets-configuration> (host-key secrets)
    (with-imported-modules (source-module-closure '((guix build utils)))
      #~(begin
          (use-modules (ice-9 format)
                       (ice-9 popen)
                       ((guix build utils) #:select (mkdir-p dump-port)))
          (define (install encrypted-file 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 OPEN_WRITE)))
              ;; 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)
              (let ((stream (open-pipe* OPEN_READ #$(file-append age "/bin/age")
                                        "-d" "-i" #$host-key encrypted-file)))
                (dump-port stream port)
                (close-pipe stream))
              (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)
            (unless (status:exit-val
                     (system* #$(file-append age-keygen "/bin/age-keygen")
                              "-o" #$host-key))
              (error "Failed to generate host key at:" #$host-key)))
          #$@(map secret-install-invocation 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)
             (match-record config <secrets-configuration> (secrets)
               (secrets-configuration
                (inherit config)
                (secrets (append secrets more-secrets))))))
   (default-value (secrets-configuration))
   (description "Install files containing secrets on the system.")))