diff options
| author | Danny Milosavljevic <dannym@friendly-machines.com> | 2026-02-08 21:50:42 +0100 |
|---|---|---|
| committer | Danny Milosavljevic <dannym@friendly-machines.com> | 2026-04-09 19:55:05 +0200 |
| commit | b90597b98d46767207a0e92a84fb39c344472b69 (patch) | |
| tree | 9278f56f2a91341f67cf203c826f729364b04d44 /gnu/system | |
| parent | 338f665b97afdb0928002aa538c21be56d1585a3 (diff) | |
guix: Ask LUKS password only once on boot when using GRUB.
* gnu/bootloader/grub.scm (make-grub-configuration): Modify.
* gnu/system/mapped-devices.scm (open-luks-device): Modify.
* gnu/tests/install.scm (%encrypted-root-installation-script): Modify.
(%encrypted-root-os): Make debugging possible.
* doc/guix.texi (Mapped Devices): Cross-reference automatic LUKS master key
passing.
(BootloaderConfiguration): Document automatic LUKS master key passing via
GRUB's (proc)/luks_script. Update extra-initrd documentation.
Change-Id: I5be74a524de04232d156e750109707fe7e50c28a
Diffstat (limited to 'gnu/system')
| -rw-r--r-- | gnu/system/mapped-devices.scm | 209 |
1 files changed, 159 insertions, 50 deletions
diff --git a/gnu/system/mapped-devices.scm b/gnu/system/mapped-devices.scm index d568bddc4ff..8c32c3d3d4d 100644 --- a/gnu/system/mapped-devices.scm +++ b/gnu/system/mapped-devices.scm | |||
| @@ -209,58 +209,162 @@ requests is allowed for the underlying device. EXTRA-OPTIONS is a list of | |||
| 209 | additional options to be passed to the 'cryptsetup open' command." | 209 | additional options to be passed to the 'cryptsetup open' command." |
| 210 | (with-imported-modules (source-module-closure | 210 | (with-imported-modules (source-module-closure |
| 211 | '((gnu build file-systems) | 211 | '((gnu build file-systems) |
| 212 | (guix base16) | ||
| 212 | (guix build utils))) ;; For mkdir-p | 213 | (guix build utils))) ;; For mkdir-p |
| 213 | (match targets | 214 | (match targets |
| 214 | ((target) | 215 | ((target) |
| 215 | #~(let ((source #$(if (uuid? source) | 216 | #~(begin |
| 216 | (uuid-bytevector source) | 217 | (use-modules (guix base16)) |
| 217 | source)) | 218 | |
| 218 | (keyfile #$key-file)) | 219 | (let ((source #$(if (uuid? source) |
| 219 | 220 | (uuid-bytevector source) | |
| 220 | ;; Create '/run/cryptsetup/' if it does not exist, as device locking | 221 | source)) |
| 221 | ;; is mandatory for LUKS2. | 222 | (keyfile #$key-file)) |
| 222 | (mkdir-p "/run/cryptsetup/") | 223 | |
| 223 | 224 | (define (luks-script-lookup-key partition) | |
| 224 | ;; Use 'cryptsetup-static', not 'cryptsetup', to avoid pulling the | 225 | "Parse /etc/luks_script and return the master key as a |
| 225 | ;; whole world inside the initrd (for when we're in an initrd). | 226 | bytevector for PARTITION. Throw to 'luks-script-error with a message |
| 226 | ;; 'cryptsetup open' requires standard input to be a tty to allow | 227 | string if the file is missing, the partition UUID cannot be read, or |
| 227 | ;; for interaction but shepherd sets standard input to /dev/null; | 228 | no matching entry is found." |
| 228 | ;; thus, explicitly request a tty. | 229 | (let ((script-file "/etc/luks_script")) |
| 229 | (let ((partition | 230 | (unless (file-exists? script-file) |
| 230 | ;; Note: We cannot use the "UUID=source" syntax here | 231 | (throw 'luks-script-error |
| 231 | ;; because 'cryptsetup' implements it by searching the | 232 | (format #f "~a not found, skipping" |
| 232 | ;; udev-populated /dev/disk/by-id directory but udev may | 233 | script-file))) |
| 233 | ;; be unavailable at the time we run this. | 234 | (let ((part-uuid-hex |
| 234 | (if (bytevector? source) | 235 | (or (and=> (read-luks-partition-uuid partition) |
| 235 | (or (let loop ((tries-left 10)) | 236 | bytevector->base16-string) |
| 236 | (and (positive? tries-left) | 237 | (throw 'luks-script-error |
| 237 | (or (find-partition-by-luks-uuid source) | 238 | (format #f |
| 238 | ;; If the underlying partition is | 239 | "could not read UUID from ~a" |
| 239 | ;; not found, try again after | 240 | partition))))) |
| 240 | ;; waiting a second, up to ten | 241 | (call-with-input-file script-file |
| 241 | ;; times. FIXME: This should be | 242 | (lambda (port) |
| 242 | ;; dealt with in a more robust way. | 243 | (let loop ((line (read-line port))) |
| 243 | (begin (sleep 1) | 244 | (when (eof-object? line) |
| 244 | (loop (- tries-left 1)))))) | 245 | (throw 'luks-script-error |
| 245 | (error "LUKS partition not found" source)) | 246 | (format #f |
| 246 | source))) | 247 | "no matching UUID ~a in ~a" |
| 247 | (let ((cryptsetup #$(file-append cryptsetup-static | 248 | part-uuid-hex script-file))) |
| 248 | "/sbin/cryptsetup")) | 249 | (match (string-tokenize line) |
| 249 | (cryptsetup-flags (cons* | 250 | (((or "luks_mount" "luks2_mount") |
| 250 | "open" "--type" "luks" | 251 | script-uuid _ ... hex-key) |
| 251 | (append | 252 | (if (string-ci=? (string-delete #\- script-uuid) |
| 252 | (if #$allow-discards? | 253 | part-uuid-hex) |
| 253 | '("--allow-discards") | 254 | (base16-string->bytevector hex-key) |
| 254 | '()) | 255 | (loop (read-line port)))) |
| 255 | '#$extra-options | 256 | (_ (loop (read-line port)))))))))) |
| 256 | (list partition #$target))))) | 257 | |
| 257 | ;; We want to fallback to the password unlock if the keyfile | 258 | (define (open-luks-with-volume-key cryptsetup-program key-bv |
| 258 | ;; fails. | 259 | partition target |
| 259 | (or (and keyfile | 260 | extra-open-flags) |
| 260 | (zero? (apply system*/tty cryptsetup | 261 | "Open LUKS device PARTITION as TARGET using volume key KEY-BV. |
| 261 | "--key-file" keyfile cryptsetup-flags))) | 262 | Return the exit status of cryptsetup. KEY-BV is securely wiped and |
| 262 | (zero? (apply system*/tty cryptsetup | 263 | the temporary key file removed regardless of outcome." |
| 263 | cryptsetup-flags)))))))))) | 264 | (let ((key-file "/run/.luks-master-key")) |
| 265 | (dynamic-wind | ||
| 266 | (const #t) | ||
| 267 | (lambda () | ||
| 268 | (call-with-port (open-file key-file "wb") | ||
| 269 | (lambda (p) (put-bytevector p key-bv))) | ||
| 270 | (chmod key-file #o400) | ||
| 271 | (apply system*/tty cryptsetup-program | ||
| 272 | "open" "--type" "luks" | ||
| 273 | "--volume-key-file" key-file | ||
| 274 | (append extra-open-flags | ||
| 275 | (list partition target)))) | ||
| 276 | (lambda () | ||
| 277 | (bytevector-fill! key-bv 0) | ||
| 278 | (when (file-exists? key-file) | ||
| 279 | (delete-file key-file)))))) | ||
| 280 | |||
| 281 | (define (try-luks-script-master-key cryptsetup-program | ||
| 282 | partition target | ||
| 283 | extra-open-flags) | ||
| 284 | "Try to open LUKS device PARTITION as TARGET using a master key | ||
| 285 | from /etc/luks_script (injected by GRUB). EXTRA-OPEN-FLAGS is a list of | ||
| 286 | additional flags to pass to 'cryptsetup open' (e.g., \"--allow-discards\"). | ||
| 287 | Return #t on success, #f if /etc/luks_script does not exist or the UUID | ||
| 288 | does not match. Any other error (parse failure, cryptsetup failure) is | ||
| 289 | NOT caught--it will be visible so bugs cannot hide." | ||
| 290 | (catch 'luks-script-error | ||
| 291 | (lambda () | ||
| 292 | (let* ((key-bv (luks-script-lookup-key partition)) | ||
| 293 | (status (open-luks-with-volume-key | ||
| 294 | cryptsetup-program key-bv | ||
| 295 | partition target extra-open-flags))) | ||
| 296 | (if (zero? status) | ||
| 297 | (begin | ||
| 298 | (format (current-error-port) | ||
| 299 | "luks-master-key: unlocked ~a as ~a~%" | ||
| 300 | partition target) | ||
| 301 | #t) | ||
| 302 | (begin | ||
| 303 | (format (current-error-port) | ||
| 304 | "luks-master-key: cryptsetup failed (status ~a) for ~a~%" | ||
| 305 | status partition) | ||
| 306 | #f)))) | ||
| 307 | (lambda (key msg . rest) | ||
| 308 | (format (current-error-port) | ||
| 309 | "luks-master-key: ~a~%" msg) | ||
| 310 | #f))) | ||
| 311 | |||
| 312 | ;; Create '/run/cryptsetup/' if it does not exist, as device locking | ||
| 313 | ;; is mandatory for LUKS2. | ||
| 314 | (mkdir-p "/run/cryptsetup/") | ||
| 315 | |||
| 316 | ;; Use 'cryptsetup-static', not 'cryptsetup', to avoid pulling the | ||
| 317 | ;; whole world inside the initrd (for when we're in an initrd). | ||
| 318 | ;; 'cryptsetup open' requires standard input to be a tty to allow | ||
| 319 | ;; for interaction but shepherd sets standard input to /dev/null; | ||
| 320 | ;; thus, explicitly request a tty. | ||
| 321 | (let ((partition | ||
| 322 | ;; Note: We cannot use the "UUID=source" syntax here | ||
| 323 | ;; because 'cryptsetup' implements it by searching the | ||
| 324 | ;; udev-populated /dev/disk/by-id directory but udev may | ||
| 325 | ;; be unavailable at the time we run this. | ||
| 326 | (if (bytevector? source) | ||
| 327 | (or (let loop ((tries-left 10)) | ||
| 328 | (and (positive? tries-left) | ||
| 329 | (or (find-partition-by-luks-uuid source) | ||
| 330 | ;; If the underlying partition is | ||
| 331 | ;; not found, try again after | ||
| 332 | ;; waiting a second, up to ten | ||
| 333 | ;; times. FIXME: This should be | ||
| 334 | ;; dealt with in a more robust way. | ||
| 335 | (begin (sleep 1) | ||
| 336 | (loop (- tries-left 1)))))) | ||
| 337 | (error "LUKS partition not found" source)) | ||
| 338 | source))) | ||
| 339 | (let ((cryptsetup #$(file-append cryptsetup-static | ||
| 340 | "/sbin/cryptsetup")) | ||
| 341 | (cryptsetup-flags (cons* | ||
| 342 | "open" "--type" "luks" | ||
| 343 | (append | ||
| 344 | (if #$allow-discards? | ||
| 345 | '("--allow-discards") | ||
| 346 | '()) | ||
| 347 | '#$extra-options | ||
| 348 | (list partition #$target))))) | ||
| 349 | ;; Try the GRUB-provided LUKS master key first (from | ||
| 350 | ;; /etc/luks_script, injected into the initrd via GRUB's | ||
| 351 | ;; newc: mechanism). This avoids prompting for the password | ||
| 352 | ;; a second time when GRUB already decrypted the same LUKS | ||
| 353 | ;; volume. Fall back to keyfile or interactive password on | ||
| 354 | ;; any failure. | ||
| 355 | (or (try-luks-script-master-key cryptsetup partition #$target | ||
| 356 | (append | ||
| 357 | (if #$allow-discards? | ||
| 358 | '("--allow-discards") | ||
| 359 | '()) | ||
| 360 | '#$extra-options)) | ||
| 361 | ;; We want to fallback to the password unlock if the | ||
| 362 | ;; keyfile fails. | ||
| 363 | (and keyfile | ||
| 364 | (zero? (apply system*/tty cryptsetup | ||
| 365 | "--key-file" keyfile cryptsetup-flags))) | ||
| 366 | (zero? (apply system*/tty cryptsetup | ||
| 367 | cryptsetup-flags))))))))))) | ||
| 264 | 368 | ||
| 265 | (define* (close-luks-device source targets #:rest _) | 369 | (define* (close-luks-device source targets #:rest _) |
| 266 | "Return a gexp that closes TARGET, a LUKS device." | 370 | "Return a gexp that closes TARGET, a LUKS device." |
| @@ -310,8 +414,13 @@ argument of `open-luks-device'") | |||
| 310 | (close close-luks-device) | 414 | (close close-luks-device) |
| 311 | (check check-luks-device) | 415 | (check check-luks-device) |
| 312 | (modules '((rnrs bytevectors) ;bytevector? | 416 | (modules '((rnrs bytevectors) ;bytevector? |
| 417 | (rnrs io ports) ;put-bytevector | ||
| 418 | (ice-9 match) ;match | ||
| 419 | (ice-9 rdelim) ;read-line | ||
| 313 | ((gnu build file-systems) | 420 | ((gnu build file-systems) |
| 314 | #:select (find-partition-by-luks-uuid system*/tty)))))) | 421 | #:select (find-partition-by-luks-uuid |
| 422 | read-luks-partition-uuid | ||
| 423 | system*/tty)))))) | ||
| 315 | 424 | ||
| 316 | (define-deprecated (luks-device-mapping-with-options #:key | 425 | (define-deprecated (luks-device-mapping-with-options #:key |
| 317 | key-file allow-discards?) | 426 | key-file allow-discards?) |
