Intro
Guix is a package management tool with support for declarative operation. By declarative, I mean I can write various objects—such as an operating system configuration—in a text file. Guix will take that text file and use it to create what I want. For example...
(define %simple-os
(operating-system
(host-name "komputilo")
(timezone "America/New_York")
(locale "en_US.UTF-8")
(bootloader (bootloader-configuration
(bootloader grub-bootloader)
(targets '("/dev/sdX"))))
(file-systems (cons (file-system
(device (file-system-label "my-root"))
(mount-point "/")
(type "ext4"))
%base-file-systems))
(users (cons (user-account
(name "alice")
(comment "Bob's sister")
(group "users")
(supplementary-groups '("wheel" "audio" "video")))
%base-user-accounts))))
Generally speaking, anything stored in these files or built from them should be considered public knowledge. Even if I decide not to share the declarative files, the outputs from building them are placed in a location called the store, unencrypted and globally readable on the hard drive. It would be trivial for an attacker to capture private information from these files.
This presents a challenge. How can we use private information (i.e. secrets) in these files without leaking said secrets?
There are three solutions I can think of.
- Create files containing secrets manually and avoid using Guix. Set permissions to limit readability.
- Use a service to store secrets in the store encrypted, then decrypt them on boot and place them in the desired location.
- Store the secrets encrypted, then run scripts to retrieve them on an as-needed basis.
In this post I will be discussing how I implement the 3rd solution. If 2 sounds interested, check out sops-guix.
Runtime Decryption and Retrieval
The Not State of the Art Solution
I store my secrets encrypted in a separate, private Git repository using password-store. For this strategy to work, the service or program being configured needs to support running a script to retrieve a secret.
Consider my git configuration. I regularly use the $ git send-email
command. While I could manually decrypt my password and paste that
when prompted, I would rather git do that automatically. Fortunately,
git supports running an arbitrary command to retrieve the password.
This is done by setting core.askPass
.
I could construct my git config like this:
;; Git does not actually support arguments for askPass.
;; For brevity I am going to pretend it does.
(define rsent-git-config-file
(plain-file "git-config"
"
[core]
askPass = pass ls Passwords/MyEmailAccount"))
# Contents of .config/git/config
[core]
askPass = pass ls Passwords/MyEmailAccount
While it works in this example, this solution is not as general purpose as it could be. This solution:
- Requires an environment where the PATH environment variable is already set.
- Makes it challenging to switch off of password-store in the future.
- Creates an unlinked dependency in my code because I have to install password-store elsewhere.
Some of these issues can be mitigated while still invoking pass directly but I believe there's a better way.
A More Guixy Way
Instead of invoking pass directly in my git config file, I go another route. In essence, I write a function that takes a "key" for what I want to retrieve. This function returns a script that, when invoked, decrypts the secret associated with said key and returns its value.
(define rsent-get-smtp-password-script
(get-secret-program-file "git-getpass" "Passwords/MyEmailAccount"))
(define rsent-git-config-file
(mixed-text-file "git-config"
"
[core]
askPass = " rsent-get-smtp-password-script "
"))
# Contents of .config/git/config
[core]
askPass = /gnu/store/...-git-getpass
get-secret-program-file
in turn looks something like this:
(define (get-secret-program-file name key)
"Returns a file-like object capable of retrieving the secret for @var{key}."
(program-file name #~(display #$(get-secret key))))
As you can probably guess, most of the magic happens in
another function called get-secret
. This function is quite a bit more complicated than I want to dive into, but I'll share an abbreviated
version. (Until recently this function was short, but then GnuPG pinentry fun happened.)
(define (get-secret key)
"Returns a @code{gexp} that returns the associated secret for @var{key}.
Key should be in the form of a password-store path."
#~(begin
(use-modules (ice-9 format)
(ice-9 popen)
(ice-9 rdelim))
(format (current-error-port) "~!Fetching secret for ~a... " #$key)
(let* ((port (open-input-pipe
(string-append
#$(file-append password-store "/bin/pass")
" ls " #$key)))
(str (read-line port)))
(close-pipe port)
(when (eof-object? str)
(error "Failed to retrieve secret for" #$key))
(format (current-error-port) "~!done~%")
str)))
I want to emphasize how flexible this solution is. Any arbitrary
Scheme code can be placed in get-secret
, invoking any combination of external programs. If in the future I
want to switch to another secret management system, I only need to
rework one function and everything that works with secrets will use
the new system.
Conclusion
Guix does not have an official integrated method for secrets management. This leads to users creating ad-hoc solutions. One such method is creating executable scripts that can decrypt and retrieve secrets, then embedding those scripts in the appropriate program or service.
Here are the positives to this approach:
- Secrets are always encrypted on disk.
- Works for both Guix System and Guix Home.
- It is easy to migrate between different methods for storing secrets.
However, there are limitations:
- The services and/or programs being configured need to support executing programs to retrieve secrets.
- User interaction is generally recommended to protect against unauthorized access.
- The creator is responsible for ensuring the script is secure, especially if it is designed to operate non-interactivity
- Debugging requires a thorough understanding of the context the script is running in. For example, system services have a barer environment than home services.