ssh.scm 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2022 Ludovic Courtès <ludo@gnu.org>
  3. ;;; Copyright © 2023 Janneke Nieuwenhuizen <janneke@gnu.org>
  4. ;;;
  5. ;;; This file is part of GNU Guix.
  6. ;;;
  7. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  8. ;;; under the terms of the GNU General Public License as published by
  9. ;;; the Free Software Foundation; either version 3 of the License, or (at
  10. ;;; your option) any later version.
  11. ;;;
  12. ;;; GNU Guix is distributed in the hope that it will be useful, but
  13. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  14. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. ;;; GNU General Public License for more details.
  16. ;;;
  17. ;;; You should have received a copy of the GNU General Public License
  18. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  19. (define-module (gnu home services ssh)
  20. #:use-module (guix gexp)
  21. #:use-module (guix records)
  22. #:use-module (guix deprecation)
  23. #:use-module (guix diagnostics)
  24. #:use-module (guix i18n)
  25. #:use-module (gnu services)
  26. #:use-module (gnu services configuration)
  27. #:use-module (guix modules)
  28. #:use-module (gnu home services)
  29. #:use-module (gnu home services shepherd)
  30. #:use-module ((gnu home services utils)
  31. #:select (object->camel-case-string))
  32. #:autoload (gnu packages base) (glibc-utf8-locales)
  33. #:use-module (gnu packages ssh)
  34. #:use-module (srfi srfi-1)
  35. #:use-module (srfi srfi-9)
  36. #:use-module (srfi srfi-9 gnu)
  37. #:use-module (srfi srfi-34)
  38. #:use-module (srfi srfi-35)
  39. #:use-module (ice-9 match)
  40. #:export (home-openssh-configuration
  41. home-openssh-configuration-authorized-keys
  42. home-openssh-configuration-known-hosts
  43. home-openssh-configuration-hosts
  44. home-ssh-agent-configuration
  45. openssh-host
  46. openssh-host-host-name
  47. openssh-host-identity-file
  48. openssh-host-name
  49. openssh-host-port
  50. openssh-host-user
  51. openssh-host-forward-x11?
  52. openssh-host-forward-x11-trusted?
  53. openssh-host-forward-agent?
  54. openssh-host-compression?
  55. openssh-host-proxy-command
  56. openssh-host-host-key-algorithms
  57. openssh-host-accepted-key-types
  58. openssh-host-extra-content
  59. proxy-jump
  60. proxy-jump-host-name
  61. proxy-jump-port
  62. proxy-jump-user
  63. proxy-command
  64. proxy-command->string
  65. home-openssh-service-type
  66. home-ssh-agent-service-type))
  67. (define (serialize-field-name name)
  68. (match name
  69. ('accepted-key-types "PubkeyAcceptedKeyTypes")
  70. (_
  71. (let ((name (let ((str (symbol->string name)))
  72. (if (string-suffix? "?" str)
  73. (string->symbol (string-drop-right str 1))
  74. name))))
  75. (object->camel-case-string name 'upper)))))
  76. (define (serialize-string field value)
  77. (string-append " " (serialize-field-name field)
  78. " " value "\n"))
  79. (define (address-family? obj)
  80. (memv obj (list AF_INET AF_INET6)))
  81. (define-maybe address-family)
  82. (define (serialize-address-family field family)
  83. (if (maybe-value-set? family)
  84. (string-append " " (serialize-field-name field) " "
  85. (cond ((= family AF_INET) "inet")
  86. ((= family AF_INET6) "inet6")
  87. ;; The 'else' branch is unreachable.
  88. (else (raise (condition (&error)))))
  89. "\n")
  90. ""))
  91. (define (natural-number? obj)
  92. (and (integer? obj) (exact? obj) (> obj 0)))
  93. (define (serialize-natural-number field value)
  94. (string-append " " (serialize-field-name field) " "
  95. (number->string value) "\n"))
  96. (define (serialize-boolean field value)
  97. (string-append " " (serialize-field-name field) " "
  98. (if value "yes" "no") "\n"))
  99. (define-maybe string)
  100. (define-maybe natural-number)
  101. (define (serialize-raw-configuration-string field value)
  102. (string-append value "\n"))
  103. (define raw-configuration-string? string?)
  104. (define (string-list? lst)
  105. (and (pair? lst) (every string? lst)))
  106. (define (serialize-string-list field lst)
  107. (string-append " " (serialize-field-name field) " "
  108. (string-join lst ",") "\n"))
  109. (define-maybe string-list)
  110. (define-record-type <proxy-command>
  111. (proxy-command command)
  112. proxy-command?
  113. (command proxy-command->string))
  114. (set-record-type-printer! <proxy-command>
  115. (lambda (obj port)
  116. (format port "#<proxy-command ~s>" (proxy-command->string obj))))
  117. (define-configuration/no-serialization proxy-jump
  118. (user
  119. maybe-string
  120. "User name on the remote host.")
  121. (host-name
  122. (string)
  123. "Host name---e.g., @code{foo.example.org} or @code{192.168.1.2}.")
  124. (port
  125. maybe-natural-number
  126. "TCP port number to connect to."))
  127. (define (proxy-jump->string proxy-jump)
  128. (match-record proxy-jump <proxy-jump>
  129. (host-name user port)
  130. (string-append
  131. (if (maybe-value-set? user) (string-append user "@") "")
  132. host-name
  133. (if (maybe-value-set? port) (string-append ":" (number->string port)) ""))))
  134. (define (proxy-command-or-jump-list? x)
  135. (or (proxy-command? x)
  136. (and (list? x)
  137. (every proxy-jump? x))))
  138. (define (serialize-proxy-command-or-jump-list field value)
  139. (if (proxy-command? value)
  140. (serialize-string 'proxy-command (proxy-command->string value))
  141. (serialize-string-list 'proxy-jump (map proxy-jump->string value))))
  142. (define-maybe proxy-command-or-jump-list)
  143. (define (sanitize-proxy-command properties)
  144. (lambda (value)
  145. (when (maybe-value-set? value)
  146. (warn-about-deprecation 'proxy-command properties #:replacement 'proxy))
  147. (unless (maybe-string? value)
  148. (configuration-field-error (source-properties->location properties) 'proxy-command value))
  149. value))
  150. (define-configuration openssh-host
  151. (name
  152. (string)
  153. "Name of this host declaration.")
  154. (host-name
  155. maybe-string
  156. "Host name---e.g., @code{\"foo.example.org\"} or @code{\"192.168.1.2\"}.")
  157. (address-family
  158. maybe-address-family
  159. "Address family to use when connecting to this host: one of
  160. @code{AF_INET} (for IPv4 only), @code{AF_INET6} (for IPv6 only).
  161. Additionally, the field can be left unset to allow any address family.")
  162. (identity-file
  163. maybe-string
  164. "The identity file to use---e.g.,
  165. @code{\"/home/charlie/.ssh/id_ed25519\"}.")
  166. (port
  167. maybe-natural-number
  168. "TCP port number to connect to.")
  169. (user
  170. maybe-string
  171. "User name on the remote host.")
  172. (forward-x11?
  173. (boolean #f)
  174. "Whether to forward remote client connections to the local X11 graphical
  175. display.")
  176. (forward-x11-trusted?
  177. (boolean #f)
  178. "Whether remote X11 clients have full access to the original X11 graphical
  179. display.")
  180. (forward-agent?
  181. (boolean #f)
  182. "Whether the authentication agent (if any) is forwarded to the remote
  183. machine.")
  184. (compression?
  185. (boolean #f)
  186. "Whether to compress data in transit.")
  187. (proxy-command
  188. maybe-string
  189. "The command to use to connect to the server. As an example, a command
  190. to connect via an HTTP proxy at 192.0.2.0 would be: @code{\"nc -X
  191. connect -x 192.0.2.0:8080 %h %p\"}. Using 'proxy-command' is deprecated, use
  192. 'proxy' instead."
  193. (sanitizer (sanitize-proxy-command (current-source-location))))
  194. (proxy
  195. maybe-proxy-command-or-jump-list
  196. "The command to use to connect to the server or a list of SSH hosts to jump
  197. through before connecting to the server.")
  198. (host-key-algorithms
  199. maybe-string-list
  200. "The list of accepted host key algorithms---e.g.,
  201. @code{'(\"ssh-ed25519\")}.")
  202. (accepted-key-types
  203. maybe-string-list
  204. "The list of accepted user public key types.")
  205. (extra-content
  206. (raw-configuration-string "")
  207. "Extra content appended as-is to this @code{Host} block in
  208. @file{~/.ssh/config}."))
  209. (define (serialize-openssh-host config)
  210. (define (openssh-host-name-field? field)
  211. (eq? (configuration-field-name field) 'name))
  212. (string-append
  213. "Host " (openssh-host-name config) "\n"
  214. (string-concatenate
  215. (map (lambda (field)
  216. ((configuration-field-serializer field)
  217. (configuration-field-name field)
  218. ((configuration-field-getter field) config)))
  219. (remove openssh-host-name-field?
  220. openssh-host-fields)))))
  221. (define-record-type* <home-openssh-configuration>
  222. home-openssh-configuration make-home-openssh-configuration
  223. home-openssh-configuration?
  224. (authorized-keys home-openssh-configuration-authorized-keys ;list of file-like
  225. (default #f))
  226. (known-hosts home-openssh-configuration-known-hosts ;unspec | list of file-like
  227. (default *unspecified*))
  228. (hosts home-openssh-configuration-hosts ;list of <openssh-host>
  229. (default '())))
  230. (define (openssh-configuration->string config)
  231. (string-join (map serialize-openssh-host
  232. (home-openssh-configuration-hosts config))
  233. "\n"))
  234. (define* (file-join name files #:optional (delimiter " "))
  235. "Return a file in the store called @var{name} that is the concatenation
  236. of all the file-like objects listed in @var{files}, with @var{delimited}
  237. inserted after each of them."
  238. (computed-file name
  239. (with-imported-modules '((guix build utils))
  240. #~(begin
  241. (use-modules (guix build utils))
  242. ;; Support non-ASCII file names.
  243. (setenv "GUIX_LOCPATH"
  244. #+(file-append glibc-utf8-locales
  245. "/lib/locale"))
  246. (setlocale LC_ALL "en_US.utf8")
  247. (call-with-output-file #$output
  248. (lambda (output)
  249. (for-each (lambda (file)
  250. (call-with-input-file file
  251. (lambda (input)
  252. (dump-port input output)))
  253. (display #$delimiter output))
  254. '#$files)))))))
  255. (define (openssh-configuration-files config)
  256. (let* ((ssh-config (plain-file "ssh.conf"
  257. (openssh-configuration->string config)))
  258. (known-hosts (home-openssh-configuration-known-hosts config))
  259. (authorized-keys (home-openssh-configuration-authorized-keys config))
  260. (authorized-keys (and
  261. authorized-keys
  262. (file-join "authorized_keys" authorized-keys "\n"))))
  263. `(,@(if authorized-keys
  264. `((".ssh/authorized_keys" ,authorized-keys))
  265. '())
  266. ,@(if (unspecified? known-hosts)
  267. '()
  268. `((".ssh/known_hosts"
  269. ,(file-join "known_hosts" known-hosts "\n"))))
  270. (".ssh/config" ,ssh-config))))
  271. (define openssh-activation
  272. (with-imported-modules (source-module-closure
  273. '((gnu build activation)))
  274. #~(begin
  275. (use-modules (gnu build activation))
  276. ;; Make sure ~/.ssh is #o700.
  277. (let* ((home (getenv "HOME"))
  278. (dot-ssh (string-append home "/.ssh")))
  279. (mkdir-p/perms dot-ssh (getpw (getuid)) #o700)))))
  280. (define home-openssh-service-type
  281. (service-type
  282. (name 'home-openssh)
  283. (extensions
  284. (list (service-extension home-files-service-type
  285. openssh-configuration-files)
  286. (service-extension home-activation-service-type
  287. (const openssh-activation))))
  288. (description "Configure the OpenSSH @acronym{SSH, secure shell} client
  289. by providing a @file{~/.ssh/config} file, which is honored by the OpenSSH
  290. client,@command{ssh}, and by other tools such as @command{guix deploy}.")
  291. (default-value (home-openssh-configuration))))
  292. ;;;
  293. ;;; Ssh-agent.
  294. ;;;
  295. (define-record-type* <home-ssh-agent-configuration>
  296. home-ssh-agent-configuration make-home-ssh-agent-configuration
  297. home-ssh-agent-configuration?
  298. (openssh home-ssh-agent-openssh ;file-like
  299. (default openssh))
  300. (socket-directory home-ssh-agent-socket-directory ;string
  301. (default #~(string-append %user-runtime-dir "/ssh-agent")))
  302. (extra-options home-ssh-agent-extra-options ;list of string
  303. (default '())))
  304. (define (home-ssh-agent-services config)
  305. "Return a <shepherd-service> for an ssh-agent with CONFIG."
  306. (match-record config <home-ssh-agent-configuration>
  307. (openssh socket-directory extra-options)
  308. (let* ((ssh-agent (file-append openssh "/bin/ssh-agent"))
  309. (socket-file #~(string-append #$socket-directory "/socket"))
  310. (command #~`(#$ssh-agent
  311. "-D" "-a" ,#$socket-file
  312. #$@extra-options))
  313. (log-file #~(string-append %user-log-dir "/ssh-agent.log")))
  314. (list (shepherd-service
  315. (documentation "Run the ssh-agent.")
  316. (provision '(ssh-agent))
  317. (modules '((shepherd support))) ;for '%user-runtime-dir', etc.
  318. (start #~(lambda _
  319. (unless (file-exists? #$socket-directory)
  320. (mkdir-p #$socket-directory)
  321. (chmod #$socket-directory #o700))
  322. (fork+exec-command #$command #:log-file #$log-file)))
  323. (stop #~(make-kill-destructor)))))))
  324. (define (home-ssh-agent-environment-variables config)
  325. '(("SSH_AUTH_SOCK"
  326. . "${SSH_AUTH_SOCK-${XDG_RUNTIME_DIR-$HOME/.cache}/ssh-agent/socket}")))
  327. (define home-ssh-agent-service-type
  328. (service-type
  329. (name 'home-ssh-agent)
  330. (default-value (home-ssh-agent-configuration))
  331. (extensions
  332. (list (service-extension home-shepherd-service-type
  333. home-ssh-agent-services)
  334. (service-extension home-environment-variables-service-type
  335. home-ssh-agent-environment-variables)))
  336. (description
  337. "Install and configure @command{ssh-agent} as a Shepherd service.")))