[PATCH] home: Add OpenSSH service.

DoneSubmitted by Ludovic Courtès.
Details
3 participants
  • Ludovic Courtès
  • Maxime Devos
  • Philip McGrath
Owner
unassigned
Severity
normal
L
L
Ludovic Courtès wrote on 11 Jun 18:49 +0200
(address . guix-patches@gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)
20220611164931.21953-1-ludo@gnu.org
* gnu/home/services/ssh.scm: New file.
* gnu/local.mk (GNU_SYSTEM_MODULES): Add it.
* po/guix/POTFILES.in: Add it.
* doc/guix.texi (Secure Shell): New section.
---
doc/guix.texi | 183 +++++++++++++++++++++++++++-
gnu/home/services/ssh.scm | 250 ++++++++++++++++++++++++++++++++++++++
gnu/local.mk | 1 +
po/guix/POTFILES.in | 1 +
4 files changed, 434 insertions(+), 1 deletion(-)
create mode 100644 gnu/home/services/ssh.scm

Hi!

Here’s an OpenSSH Home service, loosely inspired by what Julien had
implemented at:


One thing I wasn’t sure about was how to handle ~/.ssh/known_hosts.
To lower the barrier to entry, I added an option to keep handling it
in a stateful way (with ‘ssh’ updating the file as it sees fit), and
I made that the default.

I toyed with other approaches. In particular, just like Julien’s
module had <openssh-known-host>, I tried doing that and going further
so one could write:

(openssh-host-key ssh-rsa "AAAAE2VjZHNhLX…")

and arrange so that (1) the host key algorithm is validated (a typo
would be reported at macro-expansion time), and (2) the string is
base64-decoded, similar to what is done for origins.

But then, while this is perhaps The Right Thing, I though it could be
too inconvenient to use: users would have to convert what ‘ssh’ gives
them into this format. Sure, that’d give them data validation in
return, but that’s probably too little for too high a cost.

So I sticked to something simpler that allows users to pass files
as-is in ‘known-hosts’ and ‘authorized-keys’ (note that ‘authorized-keys’
in <openssh-configuration> also works that way, so it’s consistent).

Thoughts?

Thanks,
Ludo’.

Toggle diff (492 lines)
diff --git a/doc/guix.texi b/doc/guix.texi
index ea133d519a..831b8fa7c0 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -38899,6 +38899,7 @@ services)}.
 * Shells: Shells Home Services.          POSIX shells, Bash, Zsh.
 * Mcron: Mcron Home Service.             Scheduled User's Job Execution.
 * Shepherd: Shepherd Home Service.       Managing User's Daemons.
+* SSH: Secure Shell.                     Setting up the secure shell client.
 * Desktop: Desktop Home Services.        Services for graphical environments.
 @end menu
 @c In addition to that Home Services can provide
@@ -39219,7 +39220,7 @@ GNU@tie{}mcron, a daemon to run jobs at scheduled times (@pxref{Top,,,
 mcron, GNU@tie{}mcron}).  The information about system's mcron is
 applicable here (@pxref{Scheduled Job Execution}), the only difference
 for home services is that they have to be declared in a
-@code{home-envirnoment} record instead of an @code{operating-system}
+@code{home-environment} record instead of an @code{operating-system}
 record.
 
 @defvr {Scheme Variable} home-mcron-service-type
@@ -39287,6 +39288,186 @@ mechanism instead (@pxref{Shepherd Services}).
 @end table
 @end deftp
 
+@node Secure Shell
+@subsection Secure Shell
+
+@cindex secure shell client, configuration
+@cindex SSH client, configuration
+The @uref{https://www.openssh.com, OpenSSH package} includes a client,
+the @command{ssh} command, that allows you to connect to remote machines
+using the @acronym{SSH, secure shell} protocol.  With the @code{(gnu
+home services ssh)} module, you can set up OpenSSH so that it works in a
+predictable fashion, almost independently of state on the local machine.
+To do that, you instantiate @code{home-openssh-service-type} in your
+Home configuration, as explained below.
+
+@defvr {Scheme Variable} home-openssh-service-type
+This is the type of the service to set up the OpenSSH client.  It takes
+care of several things:
+
+@itemize
+@item
+adding the @code{openssh} package to your profile so the @command{ssh}
+command is readily available;
+
+@item
+providing a @file{~/.ssh/config} file based on your configuration so
+that @command{ssh} knows about hosts you regularly connect to and their
+associated parameters;
+
+@item
+providing a @file{~/.ssh/authorized_keys}, which lists public keys that
+the local SSH server, @command{sshd}, may accept to connect to this user
+account;
+
+@item
+optionally providing a @file{~/.ssh/known_hosts} file so that @file{ssh}
+can authenticate hosts you connect to.
+@end itemize
+
+Here is a sample configuration you could add to the @code{services}
+field of your @code{home-environment}:
+
+@lisp
+(home-openssh-configuration
+ (hosts (list (openssh-host (name "ci.guix.gnu.org")
+                            (user "charlie"))
+              (openssh-host (name "chbouib")
+                            (host-name "chbouib.example.org")
+                            (user "supercharlie")
+                            (port 10022))))
+ (authorized-keys (list (local-file "alice.pub"))))
+@end lisp
+
+The example above lists two hosts and their parameters.  For instance,
+running @command{ssh chbouib} will automatically connect to
+@code{chbouib.example.org} on port 10022, logging in as user
+@samp{supercharlie}.  Further, it marks the public key in
+@file{alice.pub} as authorized for incoming connections.
+
+The value associated with a @code{home-openssh-service-type} instance
+must be a @code{home-openssh-configuration} record, as describe below.
+@end defvr
+
+@deftp {Data Type} home-openssh-configuration
+This is the datatype representing the OpenSSH client and server
+configuration in one's home environment.  It contains the following
+fields:
+
+@table @asis
+@item @code{openssh} (default: @code{openssh})
+The OpenSSH package to add to the environment's profile.
+
+@item @code{hosts} (default: @code{'()})
+A list of @code{openssh-host} records specifying host names and
+associated connection parameters (see below).  This host list goes into
+@file{~/.ssh/config}, which @command{ssh} reads at startup.
+
+@item @code{known-hosts} (default: @code{*unspecified*})
+This must be either:
+
+@itemize
+@item
+@code{*unspecified*}, in which case @code{home-openssh-service-type}
+leaves it up to @command{ssh} and to the user to maintain the list of
+known hosts at @file{~/.ssh/known_hosts}, or
+
+@item
+a list of file-like objects, in which case those are concatenated and
+emitted as @file{~/.ssh/known_hosts}.
+@end itemize
+
+The @file{~/.ssh/known_hosts} contains a list of host name/host key
+pairs that allow @command{ssh} to authenticate hosts you connect to and
+to detect possible impersonation attacks.  By default, @command{ssh}
+updates it in a @dfn{TOFU, trust-on-first-use} fashion, meaning that it
+records the host's key in that file the first time you connect to it.
+This behavior is preserved when @code{known-hosts} is set to
+@code{*unspecified*}.
+
+If you instead provide a list of host keys upfront in the
+@code{known-hosts} field, your configuration becomes self-contained and
+stateless: it can be replicated elsewhere or at another point in time.
+Preparing this list can be relatively tedious though, which is why
+@code{*unspecified*} is kept as a default.
+
+@item @code{authorized-keys} (default: @code{'()})
+This must be a list of file-like objects, each of which containing an
+SSH public key that should be authorized to connect to this machine.
+
+Concretely, these files are concatenated and made available as
+@file{~/.ssh/authorized_keys}.  If an OpenSSH server, @command{sshd}, is
+running on this machine, then it @emph{may} take this file into account:
+this is what @command{sshd} does by default, but be aware that it can
+also be configured to ignore it.
+@end table
+@end deftp
+
+@c %start of fragment
+
+@deftp {Data Type} openssh-host
+Available @code{openssh-host} fields are:
+
+@table @asis
+@item @code{name} (type: string)
+Name of this host declaration.
+
+@item @code{host-name} (default: @code{disabled}) (type: maybe-string)
+Host name---e.g., @code{"foo.example.org"} or @code{"192.168.1.2"}.
+
+@item @code{address-family} (type: address-family)
+Address family to use when connecting to this host: one of
+@code{AF_INET} (for IPv4 only), @code{AF_INET6} (for IPv6 only), or
+@code{*unspecified*} (allowing any address family).
+
+@item @code{identity-file} (default: @code{disabled}) (type: maybe-string)
+The identity file to use---e.g., @code{"/home/charlie/.ssh/id_ed25519"}.
+
+@item @code{port} (default: @code{disabled}) (type: maybe-integer)
+TCP port number to connect to.
+
+@item @code{user} (default: @code{disabled}) (type: maybe-string)
+User name on the remote host.
+
+@item @code{forward-x11?} (default: @code{#f}) (type: boolean)
+Whether to forward remote client connections to the local X11 graphical
+display.
+
+@item @code{forward-x11-trusted?} (default: @code{#f}) (type: boolean)
+Whether remote X11 clients have full access to the original X11
+graphical display.
+
+@item @code{forward-agent?} (default: @code{#f}) (type: boolean)
+Whether the authentication agent (if any) is forwarded to the remote
+machine.
+
+@item @code{compression?} (default: @code{#f}) (type: boolean)
+Whether to compress data in transit.
+
+@item @code{proxy-command} (default: @code{disabled}) (type: maybe-string)
+The command to use to connect to the server.  As an example, a command
+to connect via an HTTP proxy at 192.0.2.0 would be: @code{"nc -X connect
+-x 192.0.2.0:8080 %h %p"}.
+
+@item @code{host-key-algorithms} (default: @code{disabled}) (type: maybe-string-list)
+The list of accepted host key algorithms---e.g.,
+@code{'("ssh-ed25519")}.
+
+@item @code{accepted-key-types} (default: @code{disabled}) (type: maybe-string-list)
+The list of accepted user public key types.
+
+@item @code{extra-content} (default: @code{""}) (type: raw-configuration-string)
+Extra content appended as-is to this @code{Host} block in
+@file{~/.ssh/config}.
+
+@end table
+
+@end deftp
+
+
+@c %end of fragment
+
+
 @node Desktop Home Services
 @subsection Desktop Home Services
 
diff --git a/gnu/home/services/ssh.scm b/gnu/home/services/ssh.scm
new file mode 100644
index 0000000000..162d7df960
--- /dev/null
+++ b/gnu/home/services/ssh.scm
@@ -0,0 +1,250 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2022 Ludovic Courtès <ludo@gnu.org>
+;;;
+;;; This file is part of GNU Guix.
+;;;
+;;; GNU Guix is free software; you can redistribute it and/or modify it
+;;; under the terms of the GNU General Public License as published by
+;;; the Free Software Foundation; either version 3 of the License, or (at
+;;; your option) any later version.
+;;;
+;;; GNU Guix is distributed in the hope that it will be useful, but
+;;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;;; GNU General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.
+
+(define-module (gnu home services ssh)
+  #:use-module (guix gexp)
+  #:use-module (guix records)
+  #:use-module (guix diagnostics)
+  #:use-module (guix i18n)
+  #:use-module (gnu services)
+  #:use-module (gnu services configuration)
+  #:use-module (guix modules)
+  #:use-module (gnu home services)
+  #:use-module ((gnu home services utils)
+                #:select (object->camel-case-string))
+  #:autoload   (gnu packages ssh) (openssh)
+  #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-34)
+  #:use-module (ice-9 match)
+  #:export (home-openssh-configuration
+            home-openssh-configuration-authorized-keys
+            home-openssh-configuration-known-hosts
+            home-openssh-configuration-hosts
+
+            openssh-host
+            openssh-host-host-name
+            openssh-host-identity-file
+            openssh-host-name
+            openssh-host-port
+            openssh-host-user
+            openssh-host-forward-x11?
+            openssh-host-forward-x11-trusted?
+            openssh-host-forward-agent?
+            openssh-host-compression?
+            openssh-host-proxy-command
+            openssh-host-host-key-algorithms
+            openssh-host-accepted-key-types
+            openssh-host-extra-content
+
+            home-openssh-service-type))
+
+(define (serialize-field-name name)
+  (match name
+    ('accepted-key-types "PubkeyAcceptedKeyTypes")
+    (_
+     (let ((name (let ((str (symbol->string name)))
+                   (if (string-suffix? "?" str)
+                       (string->symbol (string-drop-right str 1))
+                       name))))
+       (object->camel-case-string name 'upper)))))
+
+(define (serialize-string field value)
+  (string-append "  " (serialize-field-name field)
+                 " " value "\n"))
+
+(define (address-family? obj)
+  (memv obj (list *unspecified* AF_INET AF_INET6)))
+
+(define (serialize-address-family field family)
+  (if (unspecified? family)
+      ""
+      (string-append "  " (serialize-field-name field) " "
+                     (cond ((= family AF_INET) "inet")
+                           ((= family AF_INET6) "inet6")
+                           (else
+                            (raise
+                             (formatted-message
+                              (G_ "~s: unsupported address family")
+                              family))))
+                     "\n")))
+
+(define (serialize-integer field value)
+  (string-append "  " (serialize-field-name field) " "
+                 (number->string value) "\n"))
+
+(define (serialize-boolean field value)
+  (string-append "  " (serialize-field-name field) " "
+                 (if value "yes" "no") "\n"))
+
+(define-maybe string)
+(define-maybe integer)
+
+(define (serialize-raw-configuration-string field value)
+  (string-append value "\n"))
+(define raw-configuration-string? string?)
+
+(define (string-list? lst)
+  (and (pair? lst) (every string? lst)))
+(define (serialize-string-list field lst)
+  (string-append "  " (serialize-field-name field) " "
+                 (string-join lst ",") "\n"))
+
+(define-maybe string-list)
+
+(define-configuration openssh-host
+  (name
+   (string)
+   "Name of this host declaration.")
+  (host-name
+   (maybe-string 'disabled)
+   "Host name---e.g., @code{\"foo.example.org\"} or @code{\"192.168.1.2\"}.")
+  (address-family
+   (address-family *unspecified*)
+   "Address family to use when connecting to this host: one of
+@code{AF_INET} (for IPv4 only), @code{AF_INET6} (for IPv6 only), or
+@code{*unspecified*} (allowing any address family).")
+  (identity-file
+   (maybe-string 'disabled)
+   "The identity file to use---e.g.,
+@code{\"/home/charlie/.ssh/id_ed25519\"}.")
+  (port
+   (maybe-integer 'disabled)
+   "TCP port number to connect to.")
+  (user
+   (maybe-string 'disabled)
+   "User name on the remote host.")
+  (forward-x11?
+   (boolean #f)
+   "Whether to forward remote client connections to the local X11 graphical
+display.")
+  (forward-x11-trusted?
+   (boolean #f)
+   "Whether remote X11 clients have full access to the original X11 graphical
+display.")
+  (forward-agent?
+   (boolean #f)
+   "Whether the authentication agent (if any) is forwarded to the remote
+machine.")
+  (compression?
+   (boolean #f)
+   "Whether to compress data in transit.")
+  (proxy-command
+   (maybe-string 'disabled)
+   "The command to use to connect to the server.  As an example, a command
+to connect via an HTTP proxy at 192.0.2.0 would be: @code{\"nc -X
+connect -x 192.0.2.0:8080 %h %p\"}.")
+  (host-key-algorithms
+   (maybe-string-list 'disabled)
+   "The list of accepted host key algorithms---e.g.,
+@code{'(\"ssh-ed25519\")}.")
+  (accepted-key-types
+   (maybe-string-list 'disabled)
+   "The list of accepted user public key types.")
+  (extra-content
+   (raw-configuration-string "")
+   "Extra content appended as-is to this @code{Host} block in
+@file{~/.ssh/config}."))
+
+(define (serialize-openssh-host config)
+  (define (openssh-host-name-field? field)
+    (eq? (configuration-field-name field) 'name))
+
+  (string-append
+   "Host " (openssh-host-name config) "\n"
+   (string-concatenate
+    (map (lambda (field)
+           ((configuration-field-serializer field)
+            (configuration-field-name field)
+            ((configuration-field-getter field) config)))
+         (remove openssh-host-name-field?
+                 openssh-host-fields)))))
+
+(define-record-type* <home-openssh-configuration>
+  home-openssh-configuration make-home-openssh-configuration
+  home-openssh-configuration?
+  (openssh         home-openssh-configuration-openssh  ;file-like
+                   (default openssh))
+  (authorized-keys home-openssh-configuration-authorized-keys ;list of file-like
+                   (default '()))
+  (known-hosts     home-openssh-configuration-known-hosts ;unspec | list of file-like
+                   (default *unspecified*))
+  (hosts           home-openssh-configuration-hosts   ;list of <openssh-host>
+                   (default '())))
+
+(define (openssh-configuration->string config)
+  (string-join (map serialize-openssh-host
+                    (home-openssh-configuration-hosts config))
+               "\n"))
+
+(define* (file-join name files #:optional (delimiter " "))
+  "Return a file in the store called @var{name} that is the concatenation
+of all the file-like objects listed in @var{files}, with @var{delimited}
+inserted after each of them."
+  (computed-file name
+                 (with-imported-modules '((guix build utils))
+                   #~(begin
+                       (use-modules (guix build utils))
+                       (call-with-output-file #$output
+                         (lambda (output)
+                           (for-each (lambda (file)
+                                       (call-with-input-file file
+                                         (lambda (input)
+                                           (dump-port input output)))
+                                       (display #$delimiter output))
+                                     '#$files)))))))
+
+(define (openssh-configuration-files config)
+  (let ((config (plain-file "config" (openssh-configuration->string config)))
+        (known-hosts (home-openssh-configuration-known-hosts config))
+        (authorized-keys (file-join
+                          "authorized_keys"
+                          (home-openssh-configuration-authorized-keys config)
+                          "\n")))
+    `((".ssh/authorized_keys" ,authorized-keys)
+      ,@(if (unspecified? known-hosts)
+            '()
+            `((".ssh/known_hosts"
+               ,(file-join "known_hosts" known-hosts "\n"))))
+      (".ssh/config" ,config))))
+
+(define openssh-activation
+  (with-imported-modules (source-module-closure
+                          '((gnu build activation)))
+    #~(begin
+        (use-modules (gnu build activation))
+
+        ;; Make sure ~/.ssh is #o700.
+        (let* ((home (getenv "HOME"))
+               (dot-ssh (string-append home "/.ssh")))
+          (mkdir-p/perms dot-ssh (getpw (getuid)) #o700)))))
+
+(define home-openssh-service-type
+  (service-type
+   (name 'home-openssh)
+   (extensions
+    (list (service-extension home-files-service-type
+                             openssh-configuration-files)
+          (service-extension home-profile-service-type
+                             (compose
+                              list
+                              home-openssh-configuration-openssh))
+          (service-extension home-activation-service-type
+                             (const openssh-activation))))
+   (description "Configure the OpenSSH @acronym{SSH, secure shell}
+client and add it to the user profile.")
+   (default-value (home-openssh-configuration))))
diff --git a/gnu/local.mk b/gnu/local.mk
index d49af0d898..f3b08ffdab 100644
--- a/gnu/local.mk
+++ b/gnu/local.mk
@@ -85,6 +85,7 @@ GNU_SYSTEM_MODULES =				\
   %D%/home/services/fontutils.scm		\
   %D%/home/services/shells.scm			\
   %D%/home/services/shepherd.scm		\
+  %D%/home/services/ssh.scm			\
   %D%/home/services/mcron.scm			\
   %D%/home/services/utils.scm			\
   %D%/home/services/xdg.scm			\
diff --git a/po/guix/POTFILES.in b/po/guix/POTFILES.in
index 6b8bd92bb7..201e5dcc87 100644
--- a/po/guix/POTFILES.in
+++ b/po/guix/POTFILES.in
@@ -6,6 +6,7 @@ gnu/services.scm
 gnu/system.scm
 gnu/services/shepherd.scm
 gnu/home/services.scm
+gnu/home/services/ssh.scm
 gnu/home/services/symlink-manager.scm
 gnu/system/file-systems.scm
 gnu/system/image.scm

base-commit: 010426e2c34428d69573cdfef88239303edcab2d
-- 
2.36.1
M
M
Maxime Devos wrote on 11 Jun 21:51 +0200
d53fd0704720c45569e610345f275e225e37c4bd.camel@telenet.be
Hi,

Some comments on the code.

Ludovic Courtès schreef op za 11-06-2022 om 18:49 [+0200]:
Toggle quote (4 lines)
> +  (port
> +   (maybe-integer 'disabled)
> +   "TCP port number to connect to.")

TCP only allows natural numbers up to some bound, and in practice
implementations only support non-zero natural numbers, so maybe the
predicate can be refined a bit?

Toggle quote (3 lines)
> + (formatted-message
> + (G_ "~s: unsupported address family")

Maybe a hint:

hint: AF_INET and AF_INET6 are supported.


Toggle quote (11 lines)
> + (define (serialize-string field value)
> + (string-append " " (serialize-field-name field)
> + " " value "\n"))

> + (name
> + (string)
> + "Name of this host declaration.")
> [...]
> + (proxy-command
> + (maybe-string 'disabled)

Attila Lendvai has a patch series at 54674 that changes 'disabled' ->
*unspecified* -- I think it would be better to apply that patch series
first.

Wouldn't the value need to be escaped? Or at least a check that it
doesn't contain special characters like \n or whatever special
charaters an OpenSSH configuration has.


Toggle quote (4 lines)
>+ (define* (file-join name files #:optional (delimiter " "))
>+ "Return a file in the store called @var{name} that is the
>+ concatenation
>+ of all the file-like objects listed in @var{files}, with
@var{delimited}
Toggle quote (2 lines)
>+ inserted after each of them."

Does this work for files with non-ASCII characters and for file names
that contain non-ASCII characters?

Toggle quote (9 lines)
>+ (service-extension home-profile-service-type
>+ (compose
>+ list
>+ home-openssh-configuration-openssh))
>+ (service-extension home-activation-service-type
>+ (const openssh-activation))))
>+ (description "Configure the OpenSSH @acronym{SSH, secure shell}
>+client and _add it to the user profile_.")

(emphasis added). Why is it automagically added to the user profile?
This is considered bad practice for system services. Maybe the user
keeps all their remote communication things in a single profile, maybe
the user only uses openssh things via other tools like 'guix deploy' or
'gnome-shell-extension-gsconnect' and hence has no need for 'openssh'
in their home profile. Maybe the user never ssh's _from_ the computer
that has the openssh home configuration and only connects _to_ the
computer and hence the 'openssh' in the profile isn't necessary.

Now there are two ways to add 'openssh' to the environment: the Guix
Home equivalent of a 'packages' field and the openssh home service,
with AFAICT no mechanism for deciding which one ‘wins’ and no mechanism
for a proper error message like ‘only add the openssh package to the
profile or use the openssh home service, not both!’, which doesn't seem
ideal to me.

reetings,
Maxime.
-----BEGIN PGP SIGNATURE-----

iI0EABYKADUWIQTB8z7iDFKP233XAR9J4+4iGRcl7gUCYqTyOBccbWF4aW1lZGV2
b3NAdGVsZW5ldC5iZQAKCRBJ4+4iGRcl7hQXAP9llupRGBBOT9Q9QMHXp1Rwhtkj
cK7pPfQ+utHxRIZcTgD8ChzYWBGY5kpllAoKkfHcznomkGO+R9YLH0w4M7J0cgw=
=LcND
-----END PGP SIGNATURE-----


M
M
Maxime Devos wrote on 12 Jun 00:13 +0200
433c9db6642f69375d7b4cbc8ad48bd6f0020ff0.camel@telenet.be
Maxime Devos schreef op za 11-06-2022 om 21:51 [+0200]:

Toggle quote (2 lines)
> [remarks about automatically adding openssh the profile]

To be clear, automatically adding relevant packages to the profile from
a home (or system) service is an option (the downsides seem mild to
me), but currently things are rather inconsistent -- there are some
services that add the package, some that don't and in the past some of
us have asked contributors of services to not do the automagical adding
(at least, for System services, though I think (not 100% sure) I've
asked this for the git Home service as well?).

Greetings,
Maxime
-----BEGIN PGP SIGNATURE-----

iI0EABYKADUWIQTB8z7iDFKP233XAR9J4+4iGRcl7gUCYqUTehccbWF4aW1lZGV2
b3NAdGVsZW5ldC5iZQAKCRBJ4+4iGRcl7i1JAP9/0KoO+R2lQeDw/eaAbIq6goF4
8CoGMKs/8UwrUx8UmgD/SHJyB78t4dREoksKFpBd/QprlF9spMu5B8AnR8/Btwo=
=TfrH
-----END PGP SIGNATURE-----


L
L
Ludovic Courtès wrote on 13 Jun 11:41 +0200
(name . Maxime Devos)(address . maximedevos@telenet.be)(address . 55912@debbugs.gnu.org)
87czfcuche.fsf@gnu.org
Hi,

Maxime Devos <maximedevos@telenet.be> skribis:

Toggle quote (9 lines)
> Ludovic Courtès schreef op za 11-06-2022 om 18:49 [+0200]:
>> +  (port
>> +   (maybe-integer 'disabled)
>> +   "TCP port number to connect to.")
>
> TCP only allows natural numbers up to some bound, and in practice
> implementations only support non-zero natural numbers, so maybe the
> predicate can be refined a bit?

We could do that, though that’s more code for little in return…

Toggle quote (7 lines)
>> + (formatted-message
>> + (G_ "~s: unsupported address family")
>
> Maybe a hint:
>
> hint: AF_INET and AF_INET6 are supported.

Sure, that makes sense.

Toggle quote (7 lines)
>> + (proxy-command
>> + (maybe-string 'disabled)
>
> Attila Lendvai has a patch series at 54674 that changes 'disabled' ->
> *unspecified* -- I think it would be better to apply that patch series
> first.

I’ll take a look.

Toggle quote (4 lines)
> Wouldn't the value need to be escaped? Or at least a check that it
> doesn't contain special characters like \n or whatever special
> charaters an OpenSSH configuration has.

Oh right, it needs to be somewhat escaped; I’ll do that. I think
‘object->string’ will be a good-enough escaping mechanism, and it’ll
take care of newlines. (Doing things The Right Way would require
detailed knowledge about the grammar that OpenSSH’s parser expects.)

Toggle quote (11 lines)
>
>>+ (define* (file-join name files #:optional (delimiter " "))
>>+ "Return a file in the store called @var{name} that is the
>>+ concatenation
>>+ of all the file-like objects listed in @var{files}, with
> @var{delimited}
>>+ inserted after each of them."
>
> Does this work for files with non-ASCII characters and for file names
> that contain non-ASCII characters?

‘files’ is a list of “file-like objects”, which, by definition, have
names acceptable for the stores (so ASCII names).

That’s not a user-visible limitation since store file names are hints
more than anything else. You could have a local file, say
“courtès.pub”, and you’d do:

(local-file "courtès.pub" "that-guy.pub")

This service doesn’t change that.

Toggle quote (19 lines)
>>+ (description "Configure the OpenSSH @acronym{SSH, secure shell}
>>+client and _add it to the user profile_.")
>
> (emphasis added). Why is it automagically added to the user profile?
> This is considered bad practice for system services. Maybe the user
> keeps all their remote communication things in a single profile, maybe
> the user only uses openssh things via other tools like 'guix deploy' or
> 'gnome-shell-extension-gsconnect' and hence has no need for 'openssh'
> in their home profile. Maybe the user never ssh's _from_ the computer
> that has the openssh home configuration and only connects _to_ the
> computer and hence the 'openssh' in the profile isn't necessary.
>
> Now there are two ways to add 'openssh' to the environment: the Guix
> Home equivalent of a 'packages' field and the openssh home service,
> with AFAICT no mechanism for deciding which one ‘wins’ and no mechanism
> for a proper error message like ‘only add the openssh package to the
> profile or use the openssh home service, not both!’, which doesn't seem
> ideal to me.

All good points! I’m usually against magically extending the profile
with new packages. In this case, my reasoning was: if you’re going to
set up OpenSSH config files, that’s probably because you’re going to
need OpenSSH, so why not bring it while we’re at it? (This rationale
usually doesn’t hold for system services: just because I run ntpd
doesn’t mean I need to have it in the system profile.)

But you’re right here, so I guess I’ll just remove it.

v2 coming soon!

Thanks,
Ludo’.
M
M
Maxime Devos wrote on 13 Jun 12:51 +0200
(name . Ludovic Courtès)(address . ludo@gnu.org)(address . 55912@debbugs.gnu.org)
1b7c6c6f4529e88ed4699ba7299ae2051a3c1240.camel@telenet.be
Ludovic Courtès schreef op ma 13-06-2022 om 11:41 [+0200]:
Toggle quote (3 lines)
> ‘files’ is a list of “file-like objects”, which, by definition, have
> names acceptable for the stores (so ASCII names).

What about
(file-append (local-file "foo" #:recursive? #true) "/éclipse")?
Seems like a file-like object with a non-ASCII name, which is
acceptable to the store (the store only cares about /gnu/store/STORE-
ITEM-NAME, anything inside can be whatever).

Greetings,
Maxime.
-----BEGIN PGP SIGNATURE-----

iI0EABYKADUWIQTB8z7iDFKP233XAR9J4+4iGRcl7gUCYqcWyxccbWF4aW1lZGV2
b3NAdGVsZW5ldC5iZQAKCRBJ4+4iGRcl7g0eAP9/PqDVPDtP87Z17BjwIVh8sZ0d
cyKOTuX1UKIAFtDPigD/eMUwcjPz2pUPIvnLUaL7ZcdO31+ayUTd33+sz+30UAc=
=dqAX
-----END PGP SIGNATURE-----


L
L
Ludovic Courtès wrote on 13 Jun 14:02 +0200
(name . Maxime Devos)(address . maximedevos@telenet.be)(address . 55912@debbugs.gnu.org)
87pmjcrcuc.fsf@gnu.org
Maxime Devos <maximedevos@telenet.be> skribis:

Toggle quote (7 lines)
> Ludovic Courtès schreef op ma 13-06-2022 om 11:41 [+0200]:
>> ‘files’ is a list of “file-like objects”, which, by definition, have
>> names acceptable for the stores (so ASCII names).
>
> What about
> (file-append (local-file "foo" #:recursive? #true) "/éclipse")?

That’s fine AFAICS.

Ludo’.
M
M
Maxime Devos wrote on 13 Jun 14:38 +0200
(name . Ludovic Courtès)(address . ludo@gnu.org)(address . 55912@debbugs.gnu.org)
daac1d73171e3bf2558e4b675aab2cc5c8727f75.camel@telenet.be
Ludovic Courtès schreef op ma 13-06-2022 om 14:02 [+0200]:
Toggle quote (2 lines)
> That’s fine AFAICS.

AFACIT it isn't, because we are not setting locale things and aren't
using glibc-locales:

$ ls $(guix build -e '((@ (guix gexp) computed-file) "foo" #~(begin (mkdir #$output) (call-with-output-file (string-append #$output "/éclipse") identity))))')
'?clipse'

Greetings,
Maxime.
-----BEGIN PGP SIGNATURE-----

iI0EABYKADUWIQTB8z7iDFKP233XAR9J4+4iGRcl7gUCYqcvtRccbWF4aW1lZGV2
b3NAdGVsZW5ldC5iZQAKCRBJ4+4iGRcl7tjiAQCyF/nDVTRhvEHS0wJvIMN+6FyM
XA/mgMJNE6VSnBRBCAD/WXU6IkFJ7phtBxq+ixUvaJtWvH/y2D+HDqn6O1KmRwM=
=uixy
-----END PGP SIGNATURE-----


M
M
Maxime Devos wrote on 13 Jun 23:58 +0200
(name . Ludovic Courtès)(address . ludo@gnu.org)(address . 55912@debbugs.gnu.org)
52f35ffdfd2f0f17679662d89c515b7611fbce77.camel@telenet.be
Ludovic Courtès schreef op ma 13-06-2022 om 11:41 [+0200]:
Toggle quote (6 lines)
> > TCP only allows natural numbers up to some bound, and in practice
> > implementations only support non-zero natural numbers, so maybe the
> > predicate can be refined a bit?
>
> We could do that, though that’s more code for little in return…

Input validation is generally considered good practice. This has
recently been rediscovered in, say,

The little extra code is trivial (just an new predicate doing some
bounds checks and exact-integer?) and:

* I believe that simply implementing the tiny procedure is less
expensive than doing a proper cost-benefit analysis

* the cost is only once, it's not a recurring cost

* the cost is trivial

* the new predicate can benefit _all_ services handling network ports

* it would benefit _all_ users of OpenSSH that might make a typo
or such. Likewise for other network services.

* the benefit is not only once, it's recurring

* cost of not doing checks:
error messages that don't appear during "guix home reconfigure"
inside Guix, and instead appear later during using the new Home
from within external software even though the error was in the
Guix Home. Becomes rather complicated.

(Slight benefit, multiplied over many uses & much time -> large benefit
compared to the tiny one-time investment.)

Greetings,
Maxime.
-----BEGIN PGP SIGNATURE-----

iI0EABYKADUWIQTB8z7iDFKP233XAR9J4+4iGRcl7gUCYqezCBccbWF4aW1lZGV2
b3NAdGVsZW5ldC5iZQAKCRBJ4+4iGRcl7vwLAQC2psYFbMsijBvGZFo39FLa+TTB
GSZx4N33BqtcJOzNCgD/Yz9MxWm8WEdKgFe5WfDR0f5VbraUKgM9CFhbROd5sQg=
=dvAh
-----END PGP SIGNATURE-----


L
L
Ludovic Courtès wrote on 14 Jun 10:08 +0200
(name . Maxime Devos)(address . maximedevos@telenet.be)(address . 55912@debbugs.gnu.org)
875yl3oefi.fsf@gnu.org
Maxime Devos <maximedevos@telenet.be> skribis:

Toggle quote (9 lines)
> Ludovic Courtès schreef op ma 13-06-2022 om 11:41 [+0200]:
>> > TCP only allows natural numbers up to some bound, and in practice
>> > implementations only support non-zero natural numbers, so maybe the
>> > predicate can be refined a bit?
>>
>> We could do that, though that’s more code for little in return…
>
> Input validation is generally considered good practice.

I agree with this general statement of course (as I mentioned, I toyed
with input validation for things that are much more error-prone: public
key and host key algorithms.)

Ludo’.
L
L
Ludovic Courtès wrote on 15 Jun 22:29 +0200
[PATCH v2] home: Add OpenSSH service.
(address . 55912@debbugs.gnu.org)
20220615202951.22501-1-ludo@gnu.org
* gnu/home/services/ssh.scm: New file.
* gnu/local.mk (GNU_SYSTEM_MODULES): Add it.
* po/guix/POTFILES.in: Add it.
* doc/guix.texi (Secure Shell): New section.
---
doc/guix.texi | 176 +++++++++++++++++++++++++-
gnu/home/services/ssh.scm | 254 ++++++++++++++++++++++++++++++++++++++
gnu/local.mk | 1 +
po/guix/POTFILES.in | 1 +
4 files changed, 431 insertions(+), 1 deletion(-)
create mode 100644 gnu/home/services/ssh.scm

Hello!

Changes compared to v1:

• Use *unspecified* instead of 'disabled for unspecified field
values, relying on the new semantics that Attila introduced.

• No longer add the ‘openssh’ package to the profile.

• Support non-ASCII file names in ‘file-join’.

• Use a “natural number” type for the ‘port’ field.

Thoughts?

Ludo’.

Toggle diff (489 lines)
diff --git a/doc/guix.texi b/doc/guix.texi
index 143bf36403..35a70ba56d 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -39043,6 +39043,7 @@ services)}.
 * Shells: Shells Home Services.          POSIX shells, Bash, Zsh.
 * Mcron: Mcron Home Service.             Scheduled User's Job Execution.
 * Shepherd: Shepherd Home Service.       Managing User's Daemons.
+* SSH: Secure Shell.                     Setting up the secure shell client.
 * Desktop: Desktop Home Services.        Services for graphical environments.
 @end menu
 @c In addition to that Home Services can provide
@@ -39363,7 +39364,7 @@ GNU@tie{}mcron, a daemon to run jobs at scheduled times (@pxref{Top,,,
 mcron, GNU@tie{}mcron}).  The information about system's mcron is
 applicable here (@pxref{Scheduled Job Execution}), the only difference
 for home services is that they have to be declared in a
-@code{home-envirnoment} record instead of an @code{operating-system}
+@code{home-environment} record instead of an @code{operating-system}
 record.
 
 @defvr {Scheme Variable} home-mcron-service-type
@@ -39431,6 +39432,179 @@ mechanism instead (@pxref{Shepherd Services}).
 @end table
 @end deftp
 
+@node Secure Shell
+@subsection Secure Shell
+
+@cindex secure shell client, configuration
+@cindex SSH client, configuration
+The @uref{https://www.openssh.com, OpenSSH package} includes a client,
+the @command{ssh} command, that allows you to connect to remote machines
+using the @acronym{SSH, secure shell} protocol.  With the @code{(gnu
+home services ssh)} module, you can set up OpenSSH so that it works in a
+predictable fashion, almost independently of state on the local machine.
+To do that, you instantiate @code{home-openssh-service-type} in your
+Home configuration, as explained below.
+
+@defvr {Scheme Variable} home-openssh-service-type
+This is the type of the service to set up the OpenSSH client.  It takes
+care of several things:
+
+@itemize
+@item
+providing a @file{~/.ssh/config} file based on your configuration so
+that @command{ssh} knows about hosts you regularly connect to and their
+associated parameters;
+
+@item
+providing a @file{~/.ssh/authorized_keys}, which lists public keys that
+the local SSH server, @command{sshd}, may accept to connect to this user
+account;
+
+@item
+optionally providing a @file{~/.ssh/known_hosts} file so that @file{ssh}
+can authenticate hosts you connect to.
+@end itemize
+
+Here is a sample configuration you could add to the @code{services}
+field of your @code{home-environment}:
+
+@lisp
+(home-openssh-configuration
+ (hosts (list (openssh-host (name "ci.guix.gnu.org")
+                            (user "charlie"))
+              (openssh-host (name "chbouib")
+                            (host-name "chbouib.example.org")
+                            (user "supercharlie")
+                            (port 10022))))
+ (authorized-keys (list (local-file "alice.pub"))))
+@end lisp
+
+The example above lists two hosts and their parameters.  For instance,
+running @command{ssh chbouib} will automatically connect to
+@code{chbouib.example.org} on port 10022, logging in as user
+@samp{supercharlie}.  Further, it marks the public key in
+@file{alice.pub} as authorized for incoming connections.
+
+The value associated with a @code{home-openssh-service-type} instance
+must be a @code{home-openssh-configuration} record, as describe below.
+@end defvr
+
+@deftp {Data Type} home-openssh-configuration
+This is the datatype representing the OpenSSH client and server
+configuration in one's home environment.  It contains the following
+fields:
+
+@table @asis
+@item @code{hosts} (default: @code{'()})
+A list of @code{openssh-host} records specifying host names and
+associated connection parameters (see below).  This host list goes into
+@file{~/.ssh/config}, which @command{ssh} reads at startup.
+
+@item @code{known-hosts} (default: @code{*unspecified*})
+This must be either:
+
+@itemize
+@item
+@code{*unspecified*}, in which case @code{home-openssh-service-type}
+leaves it up to @command{ssh} and to the user to maintain the list of
+known hosts at @file{~/.ssh/known_hosts}, or
+
+@item
+a list of file-like objects, in which case those are concatenated and
+emitted as @file{~/.ssh/known_hosts}.
+@end itemize
+
+The @file{~/.ssh/known_hosts} contains a list of host name/host key
+pairs that allow @command{ssh} to authenticate hosts you connect to and
+to detect possible impersonation attacks.  By default, @command{ssh}
+updates it in a @dfn{TOFU, trust-on-first-use} fashion, meaning that it
+records the host's key in that file the first time you connect to it.
+This behavior is preserved when @code{known-hosts} is set to
+@code{*unspecified*}.
+
+If you instead provide a list of host keys upfront in the
+@code{known-hosts} field, your configuration becomes self-contained and
+stateless: it can be replicated elsewhere or at another point in time.
+Preparing this list can be relatively tedious though, which is why
+@code{*unspecified*} is kept as a default.
+
+@item @code{authorized-keys} (default: @code{'()})
+This must be a list of file-like objects, each of which containing an
+SSH public key that should be authorized to connect to this machine.
+
+Concretely, these files are concatenated and made available as
+@file{~/.ssh/authorized_keys}.  If an OpenSSH server, @command{sshd}, is
+running on this machine, then it @emph{may} take this file into account:
+this is what @command{sshd} does by default, but be aware that it can
+also be configured to ignore it.
+@end table
+@end deftp
+
+@c %start of fragment
+
+@deftp {Data Type} openssh-host
+Available @code{openssh-host} fields are:
+
+@table @asis
+@item @code{name} (type: string)
+Name of this host declaration.
+
+@item @code{host-name} (type: maybe-string)
+Host name---e.g., @code{"foo.example.org"} or @code{"192.168.1.2"}.
+
+@item @code{address-family} (type: address-family)
+Address family to use when connecting to this host: one of
+@code{AF_INET} (for IPv4 only), @code{AF_INET6} (for IPv6 only), or
+@code{*unspecified*} (allowing any address family).
+
+@item @code{identity-file} (type: maybe-string)
+The identity file to use---e.g., @code{"/home/charlie/.ssh/id_ed25519"}.
+
+@item @code{port} (type: maybe-natural-number)
+TCP port number to connect to.
+
+@item @code{user} (type: maybe-string)
+User name on the remote host.
+
+@item @code{forward-x11?} (default: @code{#f}) (type: boolean)
+Whether to forward remote client connections to the local X11 graphical
+display.
+
+@item @code{forward-x11-trusted?} (default: @code{#f}) (type: boolean)
+Whether remote X11 clients have full access to the original X11
+graphical display.
+
+@item @code{forward-agent?} (default: @code{#f}) (type: boolean)
+Whether the authentication agent (if any) is forwarded to the remote
+machine.
+
+@item @code{compression?} (default: @code{#f}) (type: boolean)
+Whether to compress data in transit.
+
+@item @code{proxy-command} (type: maybe-string)
+The command to use to connect to the server.  As an example, a command
+to connect via an HTTP proxy at 192.0.2.0 would be: @code{"nc -X connect
+-x 192.0.2.0:8080 %h %p"}.
+
+@item @code{host-key-algorithms} (type: maybe-string-list)
+The list of accepted host key algorithms---e.g.,
+@code{'("ssh-ed25519")}.
+
+@item @code{accepted-key-types} (type: maybe-string-list)
+The list of accepted user public key types.
+
+@item @code{extra-content} (default: @code{""}) (type: raw-configuration-string)
+Extra content appended as-is to this @code{Host} block in
+@file{~/.ssh/config}.
+
+@end table
+
+@end deftp
+
+
+@c %end of fragment
+
+
 @node Desktop Home Services
 @subsection Desktop Home Services
 
diff --git a/gnu/home/services/ssh.scm b/gnu/home/services/ssh.scm
new file mode 100644
index 0000000000..ff2992766c
--- /dev/null
+++ b/gnu/home/services/ssh.scm
@@ -0,0 +1,254 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2022 Ludovic Courtès <ludo@gnu.org>
+;;;
+;;; This file is part of GNU Guix.
+;;;
+;;; GNU Guix is free software; you can redistribute it and/or modify it
+;;; under the terms of the GNU General Public License as published by
+;;; the Free Software Foundation; either version 3 of the License, or (at
+;;; your option) any later version.
+;;;
+;;; GNU Guix is distributed in the hope that it will be useful, but
+;;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;;; GNU General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.
+
+(define-module (gnu home services ssh)
+  #:use-module (guix gexp)
+  #:use-module (guix records)
+  #:use-module (guix diagnostics)
+  #:use-module (guix i18n)
+  #:use-module (gnu services)
+  #:use-module (gnu services configuration)
+  #:use-module (guix modules)
+  #:use-module (gnu home services)
+  #:use-module ((gnu home services utils)
+                #:select (object->camel-case-string))
+  #:autoload   (gnu packages base) (glibc-utf8-locales)
+  #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-34)
+  #:use-module (srfi srfi-35)
+  #:use-module (ice-9 match)
+  #:export (home-openssh-configuration
+            home-openssh-configuration-authorized-keys
+            home-openssh-configuration-known-hosts
+            home-openssh-configuration-hosts
+
+            openssh-host
+            openssh-host-host-name
+            openssh-host-identity-file
+            openssh-host-name
+            openssh-host-port
+            openssh-host-user
+            openssh-host-forward-x11?
+            openssh-host-forward-x11-trusted?
+            openssh-host-forward-agent?
+            openssh-host-compression?
+            openssh-host-proxy-command
+            openssh-host-host-key-algorithms
+            openssh-host-accepted-key-types
+            openssh-host-extra-content
+
+            home-openssh-service-type))
+
+(define (serialize-field-name name)
+  (match name
+    ('accepted-key-types "PubkeyAcceptedKeyTypes")
+    (_
+     (let ((name (let ((str (symbol->string name)))
+                   (if (string-suffix? "?" str)
+                       (string->symbol (string-drop-right str 1))
+                       name))))
+       (object->camel-case-string name 'upper)))))
+
+(define (serialize-string field value)
+  (string-append "  " (serialize-field-name field)
+                 " " value "\n"))
+
+(define (address-family? obj)
+  (memv obj (list *unspecified* AF_INET AF_INET6)))
+
+(define (serialize-address-family field family)
+  (if (unspecified? family)
+      ""
+      (string-append "  " (serialize-field-name field) " "
+                     (cond ((= family AF_INET) "inet")
+                           ((= family AF_INET6) "inet6")
+                           ;; The 'else' branch is unreachable.
+                           (else (raise (condition (&error)))))
+                     "\n")))
+
+(define (natural-number? obj)
+  (and (integer? obj) (exact? obj) (> obj 0)))
+
+(define (serialize-natural-number field value)
+  (string-append "  " (serialize-field-name field) " "
+                 (number->string value) "\n"))
+
+(define (serialize-boolean field value)
+  (string-append "  " (serialize-field-name field) " "
+                 (if value "yes" "no") "\n"))
+
+(define-maybe string)
+(define-maybe natural-number)
+
+(define (serialize-raw-configuration-string field value)
+  (string-append value "\n"))
+(define raw-configuration-string? string?)
+
+(define (string-list? lst)
+  (and (pair? lst) (every string? lst)))
+(define (serialize-string-list field lst)
+  (string-append "  " (serialize-field-name field) " "
+                 (string-join lst ",") "\n"))
+
+(define-maybe string-list)
+
+(define-configuration openssh-host
+  (name
+   (string)
+   "Name of this host declaration.")
+  (host-name
+   maybe-string
+   "Host name---e.g., @code{\"foo.example.org\"} or @code{\"192.168.1.2\"}.")
+  (address-family
+   address-family
+   "Address family to use when connecting to this host: one of
+@code{AF_INET} (for IPv4 only), @code{AF_INET6} (for IPv6 only), or
+@code{*unspecified*} (allowing any address family).")
+  (identity-file
+   maybe-string
+   "The identity file to use---e.g.,
+@code{\"/home/charlie/.ssh/id_ed25519\"}.")
+  (port
+   maybe-natural-number
+   "TCP port number to connect to.")
+  (user
+   maybe-string
+   "User name on the remote host.")
+  (forward-x11?
+   (boolean #f)
+   "Whether to forward remote client connections to the local X11 graphical
+display.")
+  (forward-x11-trusted?
+   (boolean #f)
+   "Whether remote X11 clients have full access to the original X11 graphical
+display.")
+  (forward-agent?
+   (boolean #f)
+   "Whether the authentication agent (if any) is forwarded to the remote
+machine.")
+  (compression?
+   (boolean #f)
+   "Whether to compress data in transit.")
+  (proxy-command
+   maybe-string
+   "The command to use to connect to the server.  As an example, a command
+to connect via an HTTP proxy at 192.0.2.0 would be: @code{\"nc -X
+connect -x 192.0.2.0:8080 %h %p\"}.")
+  (host-key-algorithms
+   maybe-string-list
+   "The list of accepted host key algorithms---e.g.,
+@code{'(\"ssh-ed25519\")}.")
+  (accepted-key-types
+   maybe-string-list
+   "The list of accepted user public key types.")
+  (extra-content
+   (raw-configuration-string "")
+   "Extra content appended as-is to this @code{Host} block in
+@file{~/.ssh/config}."))
+
+(define (serialize-openssh-host config)
+  (define (openssh-host-name-field? field)
+    (eq? (configuration-field-name field) 'name))
+
+  (string-append
+   "Host " (openssh-host-name config) "\n"
+   (string-concatenate
+    (map (lambda (field)
+           ((configuration-field-serializer field)
+            (configuration-field-name field)
+            ((configuration-field-getter field) config)))
+         (remove openssh-host-name-field?
+                 openssh-host-fields)))))
+
+(define-record-type* <home-openssh-configuration>
+  home-openssh-configuration make-home-openssh-configuration
+  home-openssh-configuration?
+  (authorized-keys home-openssh-configuration-authorized-keys ;list of file-like
+                   (default '()))
+  (known-hosts     home-openssh-configuration-known-hosts ;unspec | list of file-like
+                   (default *unspecified*))
+  (hosts           home-openssh-configuration-hosts   ;list of <openssh-host>
+                   (default '())))
+
+(define (openssh-configuration->string config)
+  (string-join (map serialize-openssh-host
+                    (home-openssh-configuration-hosts config))
+               "\n"))
+
+(define* (file-join name files #:optional (delimiter " "))
+  "Return a file in the store called @var{name} that is the concatenation
+of all the file-like objects listed in @var{files}, with @var{delimited}
+inserted after each of them."
+  (computed-file name
+                 (with-imported-modules '((guix build utils))
+                   #~(begin
+                       (use-modules (guix build utils))
+
+                       ;; Support non-ASCII file names.
+                       (setenv "GUIX_LOCPATH"
+                               #+(file-append glibc-utf8-locales
+                                              "/lib/locale"))
+                       (setlocale LC_ALL "en_US.utf8")
+
+                       (call-with-output-file #$output
+                         (lambda (output)
+                           (for-each (lambda (file)
+                                       (call-with-input-file file
+                                         (lambda (input)
+                                           (dump-port input output)))
+                                       (display #$delimiter output))
+                                     '#$files)))))))
+
+(define (openssh-configuration-files config)
+  (let ((config (plain-file "ssh.conf"
+                            (openssh-configuration->string config)))
+        (known-hosts (home-openssh-configuration-known-hosts config))
+        (authorized-keys (file-join
+                          "authorized_keys"
+                          (home-openssh-configuration-authorized-keys config)
+                          "\n")))
+    `((".ssh/authorized_keys" ,authorized-keys)
+      ,@(if (unspecified? known-hosts)
+            '()
+            `((".ssh/known_hosts"
+               ,(file-join "known_hosts" known-hosts "\n"))))
+      (".ssh/config" ,config))))
+
+(define openssh-activation
+  (with-imported-modules (source-module-closure
+                          '((gnu build activation)))
+    #~(begin
+        (use-modules (gnu build activation))
+
+        ;; Make sure ~/.ssh is #o700.
+        (let* ((home (getenv "HOME"))
+               (dot-ssh (string-append home "/.ssh")))
+          (mkdir-p/perms dot-ssh (getpw (getuid)) #o700)))))
+
+(define home-openssh-service-type
+  (service-type
+   (name 'home-openssh)
+   (extensions
+    (list (service-extension home-files-service-type
+                             openssh-configuration-files)
+          (service-extension home-activation-service-type
+                             (const openssh-activation))))
+   (description "Configure the OpenSSH @acronym{SSH, secure shell} client
+by providing a @file{~/.ssh/config} file, which is honored by the OpenSSH
+client,@command{ssh}, and by other tools such as @command{guix deploy}.")
+   (default-value (home-openssh-configuration))))
diff --git a/gnu/local.mk b/gnu/local.mk
index 5a9edc16bb..372573d3c4 100644
--- a/gnu/local.mk
+++ b/gnu/local.mk
@@ -85,6 +85,7 @@ GNU_SYSTEM_MODULES =				\
   %D%/home/services/fontutils.scm		\
   %D%/home/services/shells.scm			\
   %D%/home/services/shepherd.scm		\
+  %D%/home/services/ssh.scm			\
   %D%/home/services/mcron.scm			\
   %D%/home/services/utils.scm			\
   %D%/home/services/xdg.scm			\
diff --git a/po/guix/POTFILES.in b/po/guix/POTFILES.in
index 6b8bd92bb7..201e5dcc87 100644
--- a/po/guix/POTFILES.in
+++ b/po/guix/POTFILES.in
@@ -6,6 +6,7 @@ gnu/services.scm
 gnu/system.scm
 gnu/services/shepherd.scm
 gnu/home/services.scm
+gnu/home/services/ssh.scm
 gnu/home/services/symlink-manager.scm
 gnu/system/file-systems.scm
 gnu/system/image.scm

base-commit: 8a04ac4b2f5d356719d896536dabc95a9520c938
-- 
2.36.1
M
M
Maxime Devos wrote on 15 Jun 22:47 +0200
d49a133833492d09c2a2845748667abe74be6ec1.camel@telenet.be
Ludovic Courtès schreef op wo 15-06-2022 om 22:29 [+0200]:
Toggle quote (11 lines)
> +  (computed-file name
> +                 (with-imported-modules '((guix build utils))
> +                   #~(begin
> +                       (use-modules (guix build utils))
> +
> +                       ;; Support non-ASCII file names.
> +                       (setenv "GUIX_LOCPATH"
> +                               #+(file-append glibc-utf8-locales
> +                                              "/lib/locale"))
> +                       (setlocale LC_ALL "en_US.utf8")

For robustness, I think it would be best to move this locale
initialisation code to the implementation of 'computed-file' itself, to
eliminate this potential pitfall entirely.

Except for 'racket' and package transformations, this does not seem to
used by any package definition (except via meson when cross-compiling),
so it doesn't seem like this would entail a world-rebuild
(unverified!).

Greetings,
Maxime.
-----BEGIN PGP SIGNATURE-----

iI0EABYKADUWIQTB8z7iDFKP233XAR9J4+4iGRcl7gUCYqpFSxccbWF4aW1lZGV2
b3NAdGVsZW5ldC5iZQAKCRBJ4+4iGRcl7n7PAP9AWpPWphRBDrOtLDW8FB5Qm+Ec
dAd9wvtm3C5fsStligEAjhAaQbJYPSNpi/6nWyZHlfiUp1L621W1zsOQi9TxNwM=
=Z72F
-----END PGP SIGNATURE-----


L
L
Ludovic Courtès wrote on 16 Jun 12:47 +0200
(name . Maxime Devos)(address . maximedevos@telenet.be)(address . 55912@debbugs.gnu.org)
87wndglwbd.fsf@gnu.org
Maxime Devos <maximedevos@telenet.be> skribis:
Toggle quote (15 lines)
> Ludovic Courtès schreef op wo 15-06-2022 om 22:29 [+0200]:
>> +  (computed-file name
>> +                 (with-imported-modules '((guix build utils))
>> +                   #~(begin
>> +                       (use-modules (guix build utils))
>> +
>> +                       ;; Support non-ASCII file names.
>> +                       (setenv "GUIX_LOCPATH"
>> +                               #+(file-append glibc-utf8-locales
>> +                                              "/lib/locale"))
>> +                       (setlocale LC_ALL "en_US.utf8")
>
> For robustness, I think it would be best to move this locale
> initialisation code to the implementation of 'computed-file' itself, to
> eliminate this potential pitfall entirely.
I’d rather have ‘computed-file’ do just what it’s documented to do; I
don’t think this kind of thing belongs there. (It’s beyond the scope of
this patch set too.)
Ideally Guile would just do the right thing without us fiddling with
locales. That is, it would default to UTF-8 rather than ASCII.
Thanks,
Ludo’.
M
M
Maxime Devos wrote on 16 Jun 14:16 +0200
(name . Ludovic Courtès)(address . ludo@gnu.org)(address . 55912@debbugs.gnu.org)
8e5bb981309815ed813fc2e304c556cc8704c622.camel@telenet.be
Toggle quote (4 lines)
> I’d rather have ‘computed-file’ do just what it’s documented to do;
> I don’t think this kind of thing belongs there (It’s beyond the scope
> of this patch set too.)

The documentation of ‘computed-file’ can be modified to document it
uses a non-broken file name encoding instead of the broken default.
Though something for a separate patch I suppose.

Ludovic Courtès schreef op do 16-06-2022 om 12:47 [+0200]:
Toggle quote (3 lines)
> Ideally Guile would just do the right thing without us fiddling with
> locales.  That is, it would default to UTF-8 rather than ASCII.

I did a quick test, and apparently Guile calls nl_langinfo to determine
the encoding, which returns ANSI_X3.4-1968, because glibc defaults to
the C locale.  So unless you want to change the encoding of the C
locale or change the default locale or override glibc's choice of
default locale in Guile, I don't think there's anything to change in
Guile?

Greetins,
Maxime.
-----BEGIN PGP SIGNATURE-----

iI0EABYKADUWIQTB8z7iDFKP233XAR9J4+4iGRcl7gUCYqsfLhccbWF4aW1lZGV2
b3NAdGVsZW5ldC5iZQAKCRBJ4+4iGRcl7k1JAP9H27kOHWwODgTFTm21oXTSZCXR
32fEMqi/6USZAv6sdQD/eMVdB3gj9wHS5dSnWS6Xz5Un5Gj0GNleq8QChprb3w0=
=LBjy
-----END PGP SIGNATURE-----


M
M
Maxime Devos wrote on 16 Jun 14:31 +0200
Re: [bug#55912] [PATCH v2] home: Add OpenSSH service.
(name . Ludovic Courtès)(address . ludo@gnu.org)(address . 55912@debbugs.gnu.org)
74707ec9078da6445803cb00904a739b71ac0d82.camel@telenet.be
Maxime Devos schreef op do 16-06-2022 om 14:16 [+0200]:
Toggle quote (5 lines)
> Ludovic Courtès schreef op do 16-06-2022 om 12:47 [+0200]:
> > Ideally Guile would just do the right thing without us fiddling
> > with locales.  That is, it would default to UTF-8 rather than
> > ASCII.

Somewhat related, I could look into separating locales from the file
name encoding in Guile (with a parameter object or something) later?
Not a solution as-is, but would be convenient in many places ...

Greetings,
Maxime.
-----BEGIN PGP SIGNATURE-----

iI0EABYKADUWIQTB8z7iDFKP233XAR9J4+4iGRcl7gUCYqsinhccbWF4aW1lZGV2
b3NAdGVsZW5ldC5iZQAKCRBJ4+4iGRcl7kjaAP0YgG8RroelnOJYgG/ot6Xybq3/
Xps5QsgDoE/VRnq9nAEAiQmmL094D0toAZcVGByytdYUqUWScAruNKvijvjAGg0=
=JcBG
-----END PGP SIGNATURE-----


L
L
Ludovic Courtès wrote on 16 Jun 18:18 +0200
Re: bug#55912: [PATCH] home: Add OpenSSH service.
(name . Maxime Devos)(address . maximedevos@telenet.be)(address . 55912@debbugs.gnu.org)
87tu8kk2dx.fsf_-_@gnu.org
Maxime Devos <maximedevos@telenet.be> skribis:

Toggle quote (7 lines)
> I did a quick test, and apparently Guile calls nl_langinfo to determine
> the encoding, which returns ANSI_X3.4-1968, because glibc defaults to
> the C locale.  So unless you want to change the encoding of the C
> locale or change the default locale or override glibc's choice of
> default locale in Guile, I don't think there's anything to change in
> Guile?

Glibc 2.35 includes the “C.UTF-8” locale; I don’t know if it’s the
default, but it will likely help.

Toggle quote (4 lines)
> Somewhat related, I could look into separating locales from the file
> name encoding in Guile (with a parameter object or something) later?
> Not a solution as-is, but would be convenient in many places ...

Yes, that too. In (guix build syscalls), there’s a variant of ‘scandir’
for instance that is locale-independent and decodes file names as UTF-8.
Nowadays that’s probably the most sensible option.

In Guile proper, it would be nice if there were a ‘%file-name-encoding’
fluid.

Thanks,
Ludo’.
M
M
Maxime Devos wrote on 16 Jun 18:45 +0200
(name . Ludovic Courtès)(address . ludo@gnu.org)(address . 55912@debbugs.gnu.org)
623fb4e4eea75f728248bb706fbed9e5677bcc22.camel@telenet.be
Ludovic Courtès schreef op do 16-06-2022 om 18:18 [+0200]:
Toggle quote (3 lines)
> In Guile proper, it would be nice if there were a ‘%file-name-encoding’
> fluid.

I was more thinking of a %file-name-encoding parameter (instead of a
fluid), but that's what I had in mind.

Greetings,
Maxime.
-----BEGIN PGP SIGNATURE-----

iI0EABYKADUWIQTB8z7iDFKP233XAR9J4+4iGRcl7gUCYqteKhccbWF4aW1lZGV2
b3NAdGVsZW5ldC5iZQAKCRBJ4+4iGRcl7mp3AP9Y1wcNHwBnHK1T9KFuDDNGeuBX
LIfP+efu4AtyHDYsSAD7B8woV3W8w0LmuSwbpcbV7+HEHHbzdnK5HVS/fZ1orQo=
=J6vo
-----END PGP SIGNATURE-----


L
L
Ludovic Courtès wrote on 17 Jun 14:32 +0200
(name . Maxime Devos)(address . maximedevos@telenet.be)(address . 55912@debbugs.gnu.org)
874k0jii6g.fsf@gnu.org
Maxime Devos <maximedevos@telenet.be> skribis:

Toggle quote (7 lines)
> Ludovic Courtès schreef op do 16-06-2022 om 18:18 [+0200]:
>> In Guile proper, it would be nice if there were a ‘%file-name-encoding’
>> fluid.
>
> I was more thinking of a %file-name-encoding parameter (instead of a
> fluid), but that's what I had in mind.

Yeah, could be (for “historical reasons”, similar interfaces such as
‘%default-port-encoding’ were fluids, that’s why I mentioned that).

Ludo’.
P
P
Philip McGrath wrote on 17 Jun 14:42 +0200
22679917.6Emhk5qWAg@avalon
On Thursday, June 16, 2022 12:45:30 PM EDT Maxime Devos wrote:
Toggle quote (8 lines)
> Ludovic Courtès schreef op do 16-06-2022 om 18:18 [+0200]:
> > In Guile proper, it would be nice if there were a ‘%file-name-encoding’
> > fluid.
>
> I was more thinking of a %file-name-encoding parameter (instead of a
> fluid), but that's what I had in mind.
>

I think the problem goes deeper than that: an R6RS string is a fixed-length
sequence of Unicode scalar values, but a path on a Unix-like system is a
sequence of non-null bytes, and on Windows is a sequence of UTF-16 code units
with possibly unpaired surrogates (aka WTF-16 [1]). That is, there are some
valid paths that can not be represented as Scheme strings.

Racket has a really nice path datatype that handles these subtleties (there
are many bad headaches if you want to be portable to Windows) while allowing
an ergonomic use of strings for the common case. [2]

Zuo has a more minimal path API which takes advantage of the fact that a Zuo
string is like a Scheme bytevector. [3] It doesn't handle all of the
complexity managed by the Racket path type, but focuses on the subset of paths
applicable to a build environment. The implementation of all of Zuo is a
single C file. [4]

-Philip

-----BEGIN PGP SIGNATURE-----

iQIzBAABCgAdFiEE9GWrrNY3rqwUFVXPygNjjfo/HHoFAmKsdsEACgkQygNjjfo/
HHqk5g/+Ir5WMAvJZP7SCprHMGjm48Qf9VpddQf/NyDi6qA7rpXj0wSOzJP3g72M
oFK30U83BMgr6RUTyXntUfDwS7S5xZcVyCCw9xsIjL95zwDtI5WtcgAg3GzANk1M
240s1JuwRI3RGX9db/1UI/5jlFb/hoFwbjkU7Glry0y2KA67itBOfGzGmiPgmaNd
LXnPwEWvhJXbTKUcjd1vrp9moz26PhmECEKygsJmJhAV6nWWQXImhWguVc6ymHLd
FFCUyvwPXXyBSolsYPVdLzIcvlj6LtQIfFjVQEaPAhdzNHYTjfXvqdDstsmDdjps
OQT85ujcVtsvCWMxK7okbP2lCQgjQ6eTQ3JimZyO9Uk6Gn6MBL95HjSBldyyLmOz
KvyCbeaAS4XImwjncj3kq5sLtc6qtVC6iRJXAhL8f1phH6Ab43k7UeZpx+ltHoDk
KRzN/VsHlN3y24JVxLqlUYlEfjyV7Y042DkEyWyCWtSExcYlumPh/8dTPeBLlDux
HiUeJZ8mH3KSpS/bxZhNbA065U243HSMVSp58+VB+5mlCGhqyfctVX0sbCAFIdUu
VomBGz74Mq+LOxCRBUPGOjJ/+/6OMQ+UMRVrhbhLY/sE8f73xgoDcqShqQdhdjR5
CV5LLaUXyt5l/0+10cGdpHtU1A+bUDpRBEQkMNtvEVZRYq3Ivqc=
=ArPA
-----END PGP SIGNATURE-----


M
M
Maxime Devos wrote on 17 Jun 22:56 +0200
(address . 55912@debbugs.gnu.org)
f8a0549146e4eb716322f72bf7940529e13e04d6.camel@telenet.be
Philip McGrath schreef op vr 17-06-2022 om 08:42 [-0400]:
Toggle quote (4 lines)
> I think the problem goes deeper than that: an R6RS string is a fixed-
> length sequence of Unicode scalar values, but a path on a Unix-like
> system is a sequence of non-null bytes,

That's one of the possibilities I know about. Treating that case as
‘ISO-88591-1’ would be sufficient for Guix, albeit conceptually
incorrect.

Toggle quote (5 lines)
> and on Windows is a sequence of UTF-16 code units
> with possibly unpaired surrogates (aka WTF-16 [1]).
> That is, there are some
> valid paths that can not be represented as Scheme strings.

Nasty. Didn't know about that. I'll skip that one for now though
(I don't think Guile uses the right Windows APIs for that, it just
calls 'open' and 'stat' and the like).

Toggle quote (4 lines)
> Racket has a really nice path datatype that handles these subtleties
> (there are many bad headaches if you want to be portable to Windows)
> while allowing an ergonomic use of strings for the common case. [2]

For now(later), I'll just stick to support overriding the file name
encoding, a proper separate path datatype can be added later. E.g.,
open-file can be changed to support both filenames as strings (to be
encoded by the file name encoding) or as an instance of the path
datatype. Except for rebase conflicts, this seems rather orthogonal to
me.

Greetings,
Maxime.
-----BEGIN PGP SIGNATURE-----

iI0EABYKADUWIQTB8z7iDFKP233XAR9J4+4iGRcl7gUCYqzqlBccbWF4aW1lZGV2
b3NAdGVsZW5ldC5iZQAKCRBJ4+4iGRcl7tAUAP9Rw/tcj9PRlYS1q3e3nWDX3Vxy
l2Zc39svxCXPhuDSPgEAt+z0AwzviY33ALH7FbV/sBjcvXltOjTrcdNZV9ISQQo=
=7ep8
-----END PGP SIGNATURE-----


L
L
Ludovic Courtès wrote on 18 Jun 23:41 +0200
(address . 55912-done@debbugs.gnu.org)(name . Maxime Devos)(address . maximedevos@telenet.be)
87r13ld4yw.fsf_-_@gnu.org
Ludovic Courtès <ludo@gnu.org> skribis:

Toggle quote (5 lines)
> * gnu/home/services/ssh.scm: New file.
> * gnu/local.mk (GNU_SYSTEM_MODULES): Add it.
> * po/guix/POTFILES.in: Add it.
> * doc/guix.texi (Secure Shell): New section.

Pushed yesterday as 7f208f68dea828fe02718ca8ce81d5975136cff8.

Thanks, Maxime!

Ludo’.
Closed
?
Your comment

This issue is archived.

To comment on this conversation send email to 55912@debbugs.gnu.org