X-Git-Url: https://git.hcoop.net/jackhill/guix/guix.git/blobdiff_plain/47ddf3e5f07351c738d6199a0df540d193d14e31..533935ccf71bef93f2f146fba1b2bb6402559965:/gnu/services/ssh.scm diff --git a/gnu/services/ssh.scm b/gnu/services/ssh.scm index 7b2d4a8f06..1e45495e1b 100644 --- a/gnu/services/ssh.scm +++ b/gnu/services/ssh.scm @@ -1,8 +1,11 @@ ;;; GNU Guix --- Functional package management for GNU -;;; Copyright © 2014, 2015, 2016 Ludovic Courtès +;;; Copyright © 2014, 2015, 2016, 2017, 2018, 2019, 2020 Ludovic Courtès ;;; Copyright © 2016 David Craven ;;; Copyright © 2016 Julien Lepiller ;;; Copyright © 2017 Clément Lassieur +;;; Copyright © 2019 Ricardo Wurmus +;;; Copyright © 2020 pinoaffe +;;; Copyright © 2020 Oleg Pykhalov ;;; ;;; This file is part of GNU Guix. ;;; @@ -24,10 +27,16 @@ #:use-module (gnu packages admin) #:use-module (gnu services) #:use-module (gnu services shepherd) + #:use-module (gnu services web) #:use-module (gnu system pam) #:use-module (gnu system shadow) #:use-module (guix gexp) #:use-module (guix records) + #:use-module (guix modules) + #:use-module ((guix i18n) #:select (G_)) + #:use-module ((guix diagnostics) #:select (warning source-properties->location)) + #:use-module ((guix memoization) #:select (mlambda)) + #:use-module (srfi srfi-1) #:use-module (srfi srfi-26) #:use-module (ice-9 match) #:export (lsh-configuration @@ -42,7 +51,16 @@ dropbear-configuration dropbear-configuration? dropbear-service-type - dropbear-service)) + dropbear-service + + autossh-configuration + autossh-configuration? + autossh-service-type + + webssh-configuration + webssh-configuration? + webssh-service-type + %webssh-configuration-nginx)) ;;; Commentary: ;;; @@ -170,7 +188,7 @@ (list (shepherd-service (documentation "GNU lsh SSH server") - (provision '(ssh-daemon)) + (provision '(ssh-daemon ssh sshd)) (requirement requires) (start #~(make-forkexec-constructor (list #$@lsh-command))) (stop #~(make-kill-destructor))))) @@ -179,11 +197,15 @@ "Return a list of for lshd with CONFIG." (list (unix-pam-service "lshd" + #:login-uid? #t #:allow-empty-passwords? (lsh-configuration-allow-empty-passwords? config)))) (define lsh-service-type (service-type (name 'lsh) + (description + "Run the GNU@tie{}lsh secure shell (SSH) daemon, +@command{lshd}.") (extensions (list (service-extension shepherd-root-service-type lsh-shepherd-service) @@ -257,27 +279,94 @@ The other options should be self-descriptive." ;;; OpenSSH. ;;; +(define true-but-soon-false + (mlambda (loc) + ;; The plan is to change the default 'password-authentication?' to #f in + ;; Guix 1.3.0 or so. See . + (warning (source-properties->location loc) + (G_ "The default value of the 'password-authentication?' +field of 'openssh-configuration' will change from #true to #false in the +future. Explicitly set it to #true to allow password authentication.~%")) + #t)) + (define-record-type* openssh-configuration make-openssh-configuration openssh-configuration? + ;; + (openssh openssh-configuration-openssh + (default openssh)) + ;; string (pid-file openssh-configuration-pid-file (default "/var/run/sshd.pid")) - (port-number openssh-configuration-port-number ;integer + ;; integer + (port-number openssh-configuration-port-number (default 22)) - (permit-root-login openssh-configuration-permit-root-login ;Boolean | 'without-password + ;; Boolean | 'without-password + (permit-root-login openssh-configuration-permit-root-login (default #f)) - (allow-empty-passwords? openssh-configuration-allow-empty-passwords? ;Boolean + ;; Boolean + (allow-empty-passwords? openssh-configuration-allow-empty-passwords? (default #f)) - (password-authentication? openssh-configuration-password-authentication? ;Boolean - (default #t)) + ;; Boolean + (password-authentication? openssh-configuration-password-authentication? + (default (true-but-soon-false + (current-source-location)))) + ;; Boolean (public-key-authentication? openssh-configuration-public-key-authentication? - (default #t)) ;Boolean - (rsa-authentication? openssh-configuration-rsa-authentication? ;Boolean + (default #t)) + ;; Boolean + (x11-forwarding? openssh-configuration-x11-forwarding? + (default #f)) + + ;; Boolean + (allow-agent-forwarding? openssh-configuration-allow-agent-forwarding? + (default #t)) + + ;; Boolean + (allow-tcp-forwarding? openssh-configuration-allow-tcp-forwarding? (default #t)) - (x11-forwarding? openssh-configuration-x11-forwarding? ;Boolean + + ;; Boolean + (gateway-ports? openssh-configuration-gateway-ports? (default #f)) - (protocol-number openssh-configuration-protocol-number ;integer - (default 2))) + + ;; Boolean + (challenge-response-authentication? openssh-challenge-response-authentication? + (default #f)) + ;; Boolean + (use-pam? openssh-configuration-use-pam? + (default #t)) + ;; Boolean + (print-last-log? openssh-configuration-print-last-log? + (default #t)) + ;; list of two-element lists + (subsystems openssh-configuration-subsystems + (default '(("sftp" "internal-sftp")))) + + ;; list of strings + (accepted-environment openssh-configuration-accepted-environment + (default '())) + + ;; symbol + (log-level openssh-configuration-log-level + (default 'info)) + + ;; String + ;; This is an "escape hatch" to provide configuration that isn't yet + ;; supported by this configuration record. + (extra-content openssh-configuration-extra-content + (default "")) + + ;; list of user-name/file-like tuples + (authorized-keys openssh-authorized-keys + (default '())) + + ;; Boolean + ;; XXX: This should really be handled in an orthogonal way, for instance as + ;; proposed in . Keep it internal/undocumented + ;; for now. + (%auto-start? openssh-auto-start? + (default #t))) (define %openssh-accounts (list (user-group (name "sshd") (system? #t)) @@ -287,53 +376,139 @@ The other options should be self-descriptive." (system? #t) (comment "sshd privilege separation user") (home-directory "/var/run/sshd") - (shell #~(string-append #$shadow "/sbin/nologin"))))) + (shell (file-append shadow "/sbin/nologin"))))) (define (openssh-activation config) "Return the activation GEXP for CONFIG." - #~(begin - (use-modules (guix build utils)) - (mkdir-p "/etc/ssh") - (mkdir-p (dirname #$(openssh-configuration-pid-file config))) - - ;; Generate missing host keys. - (system* (string-append #$openssh "/bin/ssh-keygen") "-A"))) + (with-imported-modules '((guix build utils)) + #~(begin + (use-modules (guix build utils)) + + (define (touch file-name) + (call-with-output-file file-name (const #t))) + + ;; Make sure /etc/ssh can be read by the 'sshd' user. + (mkdir-p "/etc/ssh") + (chmod "/etc/ssh" #o755) + (mkdir-p (dirname #$(openssh-configuration-pid-file config))) + + ;; 'sshd' complains if the authorized-key directory and its parents + ;; are group-writable, which rules out /gnu/store. Thus we copy the + ;; authorized-key directory to /etc. + (catch 'system-error + (lambda () + (delete-file-recursively "/etc/authorized_keys.d")) + (lambda args + (unless (= ENOENT (system-error-errno args)) + (apply throw args)))) + (copy-recursively #$(authorized-key-directory + (openssh-authorized-keys config)) + "/etc/ssh/authorized_keys.d") + + (chmod "/etc/ssh/authorized_keys.d" #o555) + + (let ((lastlog "/var/log/lastlog")) + (when #$(openssh-configuration-print-last-log? config) + (unless (file-exists? lastlog) + (touch lastlog)))) + + ;; Generate missing host keys. + (system* (string-append #$(openssh-configuration-openssh config) + "/bin/ssh-keygen") "-A")))) + +(define (authorized-key-directory keys) + "Return a directory containing the authorized keys specified in KEYS, a list +of user-name/file-like tuples." + (define build + (with-imported-modules (source-module-closure '((guix build utils))) + #~(begin + (use-modules (ice-9 match) (srfi srfi-26) + (guix build utils)) + + (mkdir #$output) + (for-each (match-lambda + ((user keys ...) + (let ((file (string-append #$output "/" user))) + (call-with-output-file file + (lambda (port) + (for-each (lambda (key) + (call-with-input-file key + (cut dump-port <> port))) + keys)))))) + '#$keys)))) + + (computed-file "openssh-authorized-keys" build)) (define (openssh-config-file config) "Return the sshd configuration file corresponding to CONFIG." (computed-file "sshd_config" - #~(call-with-output-file #$output - (lambda (port) - (display "# Generated by 'openssh-service'.\n" port) - (format port "Protocol ~a\n" - #$(if (eq? (openssh-configuration-protocol-number config) 1) - "1" "2")) - (format port "Port ~a\n" - #$(number->string (openssh-configuration-port-number config))) - (format port "PermitRootLogin ~a\n" - #$(match (openssh-configuration-permit-root-login config) - (#t "yes") - (#f "no") - ('without-password "without-password"))) - (format port "PermitEmptyPasswords ~a\n" - #$(if (openssh-configuration-allow-empty-passwords? config) - "yes" "no")) - (format port "PasswordAuthentication ~a\n" - #$(if (openssh-configuration-password-authentication? config) - "yes" "no")) - (format port "PubkeyAuthentication ~a\n" - #$(if (openssh-configuration-public-key-authentication? config) - "yes" "no")) - (format port "RSAAuthentication ~a\n" - #$(if (openssh-configuration-rsa-authentication? config) - "yes" "no")) - (format port "X11Forwarding ~a\n" - #$(if (openssh-configuration-x11-forwarding? config) - "yes" "no")) - (format port "PidFile ~a\n" - #$(openssh-configuration-pid-file config)) - #t)))) + #~(begin + (use-modules (ice-9 match)) + (call-with-output-file #$output + (lambda (port) + (display "# Generated by 'openssh-service'.\n" port) + (format port "Port ~a\n" + #$(number->string + (openssh-configuration-port-number config))) + (format port "PermitRootLogin ~a\n" + #$(match (openssh-configuration-permit-root-login config) + (#t "yes") + (#f "no") + ('without-password "without-password"))) + (format port "PermitEmptyPasswords ~a\n" + #$(if (openssh-configuration-allow-empty-passwords? config) + "yes" "no")) + (format port "PasswordAuthentication ~a\n" + #$(if (openssh-configuration-password-authentication? config) + "yes" "no")) + (format port "PubkeyAuthentication ~a\n" + #$(if (openssh-configuration-public-key-authentication? + config) + "yes" "no")) + (format port "X11Forwarding ~a\n" + #$(if (openssh-configuration-x11-forwarding? config) + "yes" "no")) + (format port "AllowAgentForwarding ~a\n" + #$(if (openssh-configuration-allow-agent-forwarding? config) + "yes" "no")) + (format port "AllowTcpForwarding ~a\n" + #$(if (openssh-configuration-allow-tcp-forwarding? config) + "yes" "no")) + (format port "GatewayPorts ~a\n" + #$(if (openssh-configuration-gateway-ports? config) + "yes" "no")) + (format port "PidFile ~a\n" + #$(openssh-configuration-pid-file config)) + (format port "ChallengeResponseAuthentication ~a\n" + #$(if (openssh-challenge-response-authentication? config) + "yes" "no")) + (format port "UsePAM ~a\n" + #$(if (openssh-configuration-use-pam? config) + "yes" "no")) + (format port "PrintLastLog ~a\n" + #$(if (openssh-configuration-print-last-log? config) + "yes" "no")) + (format port "LogLevel ~a\n" + #$(string-upcase + (symbol->string + (openssh-configuration-log-level config)))) + + ;; Add '/etc/authorized_keys.d/%u', which we populate. + (format port "AuthorizedKeysFile \ + .ssh/authorized_keys .ssh/authorized_keys2 /etc/ssh/authorized_keys.d/%u\n") + + (for-each (lambda (s) (format port "AcceptEnv ~a\n" s)) + '#$(openssh-configuration-accepted-environment config)) + + (for-each + (match-lambda + ((name command) (format port "Subsystem\t~a\t~a\n" name command))) + '#$(openssh-configuration-subsystems config)) + + (format port "~a\n" + #$(openssh-configuration-extra-content config)) + #t))))) (define (openssh-shepherd-service config) "Return a for openssh with CONFIG." @@ -342,26 +517,57 @@ The other options should be self-descriptive." (openssh-configuration-pid-file config)) (define openssh-command - #~(list (string-append #$openssh "/sbin/sshd") + #~(list (string-append #$(openssh-configuration-openssh config) "/sbin/sshd") "-D" "-f" #$(openssh-config-file config))) (list (shepherd-service (documentation "OpenSSH server.") - (requirement '(networking syslogd)) - (provision '(ssh-daemon)) + (requirement '(syslogd loopback)) + (provision '(ssh-daemon ssh sshd)) (start #~(make-forkexec-constructor #$openssh-command #:pid-file #$pid-file)) - (stop #~(make-kill-destructor))))) + (stop #~(make-kill-destructor)) + (auto-start? (openssh-auto-start? config))))) + +(define (openssh-pam-services config) + "Return a list of for sshd with CONFIG." + (list (unix-pam-service + "sshd" + #:login-uid? #t + #:allow-empty-passwords? + (openssh-configuration-allow-empty-passwords? config)))) + +(define (extend-openssh-authorized-keys config keys) + "Extend CONFIG with the extra authorized keys listed in KEYS." + (openssh-configuration + (inherit config) + (authorized-keys + (append (openssh-authorized-keys config) keys)))) (define openssh-service-type (service-type (name 'openssh) + (description + "Run the OpenSSH secure shell (SSH) server, @command{sshd}.") (extensions (list (service-extension shepherd-root-service-type openssh-shepherd-service) + (service-extension pam-root-service-type + openssh-pam-services) (service-extension activation-service-type openssh-activation) (service-extension account-service-type - (const %openssh-accounts)))))) + (const %openssh-accounts)) + + ;; Install OpenSSH in the system profile. That way, + ;; 'scp' is found when someone tries to copy to or from + ;; this machine. + (service-extension profile-service-type + (lambda (config) + (list (openssh-configuration-openssh + config)))))) + (compose concatenate) + (extend extend-openssh-authorized-keys) + (default-value (openssh-configuration)))) ;;; @@ -426,18 +632,21 @@ The other options should be self-descriptive." (list (shepherd-service (documentation "Dropbear SSH server.") (requirement requires) - (provision '(ssh-daemon)) + (provision '(ssh-daemon ssh sshd)) (start #~(make-forkexec-constructor #$dropbear-command #:pid-file #$pid-file)) (stop #~(make-kill-destructor))))) (define dropbear-service-type (service-type (name 'dropbear) + (description + "Run the Dropbear secure shell (SSH) server.") (extensions (list (service-extension shepherd-root-service-type dropbear-shepherd-service) (service-extension activation-service-type - dropbear-activation))))) + dropbear-activation))) + (default-value (dropbear-configuration)))) (define* (dropbear-service #:optional (config (dropbear-configuration))) "Run the @uref{https://matt.ucc.asn.au/dropbear/dropbear.html,Dropbear SSH @@ -445,4 +654,225 @@ daemon} with the given @var{config}, a @code{} object." (service dropbear-service-type config)) + +;;; +;;; AutoSSH. +;;; + + +(define-record-type* + autossh-configuration make-autossh-configuration + autossh-configuration? + (user autossh-configuration-user + (default "autossh")) + (poll autossh-configuration-poll + (default 600)) + (first-poll autossh-configuration-first-poll + (default #f)) + (gate-time autossh-configuration-gate-time + (default 30)) + (log-level autossh-configuration-log-level + (default 1)) + (max-start autossh-configuration-max-start + (default #f)) + (message autossh-configuration-message + (default "")) + (port autossh-configuration-port + (default "0")) + (ssh-options autossh-configuration-ssh-options + (default '()))) + +(define (autossh-file-name config file) + "Return a path in /var/run/autossh/ that is writable + by @code{user} from @code{config}." + (string-append "/var/run/autossh/" + (autossh-configuration-user config) + "/" file)) + +(define (autossh-shepherd-service config) + (shepherd-service + (documentation "Automatically set up ssh connections (and keep them alive).") + (provision '(autossh)) + (start #~(make-forkexec-constructor + (list #$(file-append autossh "/bin/autossh") + #$@(autossh-configuration-ssh-options config)) + #:user #$(autossh-configuration-user config) + #:group (passwd:gid (getpw #$(autossh-configuration-user config))) + #:pid-file #$(autossh-file-name config "pid") + #:log-file #$(autossh-file-name config "log") + #:environment-variables + '(#$(string-append "AUTOSSH_PIDFILE=" + (autossh-file-name config "pid")) + #$(string-append "AUTOSSH_LOGFILE=" + (autossh-file-name config "log")) + #$(string-append "AUTOSSH_POLL=" + (number->string + (autossh-configuration-poll config))) + #$(string-append "AUTOSSH_FIRST_POLL=" + (number->string + (or + (autossh-configuration-first-poll config) + (autossh-configuration-poll config)))) + #$(string-append "AUTOSSH_GATETIME=" + (number->string + (autossh-configuration-gate-time config))) + #$(string-append "AUTOSSH_LOGLEVEL=" + (number->string + (autossh-configuration-log-level config))) + #$(string-append "AUTOSSH_MAXSTART=" + (number->string + (or (autossh-configuration-max-start config) + -1))) + #$(string-append "AUTOSSH_MESSAGE=" + (autossh-configuration-message config)) + #$(string-append "AUTOSSH_PORT=" + (autossh-configuration-port config))))) + (stop #~(make-kill-destructor)))) + +(define (autossh-service-activation config) + (with-imported-modules '((guix build utils)) + #~(begin + (use-modules (guix build utils)) + (define %user + (getpw #$(autossh-configuration-user config))) + (let* ((directory #$(autossh-file-name config "")) + (log (string-append directory "/log"))) + (mkdir-p directory) + (chown directory (passwd:uid %user) (passwd:gid %user)) + (call-with-output-file log (const #t)) + (chown log (passwd:uid %user) (passwd:gid %user)))))) + +(define autossh-service-type + (service-type + (name 'autossh) + (description "Automatically set up ssh connections (and keep them alive).") + (extensions + (list (service-extension shepherd-root-service-type + (compose list autossh-shepherd-service)) + (service-extension activation-service-type + autossh-service-activation))) + (default-value (autossh-configuration)))) + + +;;; +;;; WebSSH +;;; + +(define-record-type* + webssh-configuration make-webssh-configuration + webssh-configuration? + (package webssh-configuration-package ;package + (default webssh)) + (user-name webssh-configuration-user-name ;string + (default "webssh")) + (group-name webssh-configuration-group-name ;string + (default "webssh")) + (policy webssh-configuration-policy ;symbol + (default #f)) + (known-hosts webssh-configuration-known-hosts ;list of strings + (default #f)) + (port webssh-configuration-port ;number + (default #f)) + (address webssh-configuration-address ;string + (default #f)) + (log-file webssh-configuration-log-file ;string + (default "/var/log/webssh.log")) + (log-level webssh-configuration-log-level ;symbol + (default #f))) + +(define %webssh-configuration-nginx + (nginx-server-configuration + (listen '("80")) + (locations + (list (nginx-location-configuration + (uri "/") + (body '("proxy_pass http://127.0.0.1:8888;" + "proxy_http_version 1.1;" + "proxy_read_timeout 300;" + "proxy_set_header Upgrade $http_upgrade;" + "proxy_set_header Connection \"upgrade\";" + "proxy_set_header Host $http_host;" + "proxy_set_header X-Real-IP $remote_addr;" + "proxy_set_header X-Real-PORT $remote_port;"))))))) + +(define webssh-account + ;; Return the user accounts and user groups for CONFIG. + (match-lambda + (($ _ user-name group-name _ _ _ _ _ _) + (list (user-group + (name group-name)) + (user-account + (name user-name) + (group group-name) + (comment "webssh privilege separation user") + (home-directory (string-append "/var/run/" user-name)) + (shell #~(string-append #$shadow "/sbin/nologin"))))))) + +(define webssh-activation + ;; Return the activation GEXP for CONFIG. + (match-lambda + (($ _ user-name group-name policy known-hosts _ _ + log-file _) + (with-imported-modules '((guix build utils)) + #~(begin + (let* ((home-dir (string-append "/var/run/" #$user-name)) + (ssh-dir (string-append home-dir "/.ssh")) + (known-hosts-file (string-append ssh-dir "/known_hosts"))) + (call-with-output-file #$log-file (const #t)) + (mkdir-p ssh-dir) + (case '#$policy + ((reject) + (if '#$known-hosts + (call-with-output-file known-hosts-file + (lambda (port) + (for-each (lambda (host) (display host port) (newline port)) + '#$known-hosts))) + (display-hint (G_ "webssh: reject policy requires `known-hosts'."))))) + (for-each (lambda (file) + (chown file + (passwd:uid (getpw #$user-name)) + (group:gid (getpw #$group-name)))) + (list #$log-file ssh-dir known-hosts-file)) + (chmod ssh-dir #o700))))))) + +(define webssh-shepherd-service + (match-lambda + (($ package user-name group-name policy _ port + address log-file log-level) + (list (shepherd-service + (provision '(webssh)) + (documentation "Run webssh daemon.") + (start #~(make-forkexec-constructor + `(,(string-append #$webssh "/bin/wssh") + ,(string-append "--log-file-prefix=" #$log-file) + ,@(case '#$log-level + ((debug) '("--logging=debug")) + (else '())) + ,@(case '#$policy + ((reject) '("--policy=reject")) + (else '())) + ,@(if #$port + (list (string-append "--port=" (number->string #$port))) + '()) + ,@(if #$address + (list (string-append "--address=" #$address)) + '())) + #:user #$user-name + #:group #$group-name)) + (stop #~(make-kill-destructor))))))) + +(define webssh-service-type + (service-type + (name 'webssh) + (extensions + (list (service-extension shepherd-root-service-type + webssh-shepherd-service) + (service-extension account-service-type + webssh-account) + (service-extension activation-service-type + webssh-activation))) + (default-value (webssh-configuration)) + (description + "Run the webssh."))) + ;;; ssh.scm ends here