;;; Consfigurator -- Lisp declarative configuration management system ;;; Copyright (C) 2021 Sean Whitton ;;; This file 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, or (at your option) ;;; any later version. ;;; This file 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 this program. If not, see . (in-package :consfigurator.property.ssh) (named-readtables:in-readtable :consfigurator) (defprop authorized-keys :posix (&rest keys) "Permits using KEYS to SSH in as the current user." (:desc (declare (ignore keys)) (strcat (get-connattr :remote-user) " has authorized_keys")) (:apply (apply #'file:contains-lines ".ssh/authorized_keys" keys)) (:unapply (apply #'file:lacks-lines ".ssh/authorized_keys" keys))) (defpropspec has-user-key :posix (dest public-key &key iden1) "Installs an SSH keypair to DEST and DEST.pub." ;; The original version of this property took a key type argument and ;; defaulted DEST to ~/.ssh/id_TYPE, but FILE:HOST-SECRET-UPLOADED requires ;; an absolute path because the remote HOME is not known at :HOSTATTRS time, ;; and the same applies here, so the caller must supply DEST. In the ;; FILE:SECRET-UPLOADED branch we could use a relative path, but we should ;; not use an identical relative path for both IDEN2 and the destination ;; when IDEN1 is a hostname, which it might be. `(eseqprops (file:exists-with-content ,(strcat (unix-namestring dest) ".pub") ,public-key) ,(if iden1 `(file:secret-uploaded ,iden1 ,dest ,dest) `(file:host-secret-uploaded ,dest)))) (defun get-host-public-keys (host &key short-hostname (aliases t) (ips t) additional-names) (let* ((host (preprocess-host host)) (hostname (get-hostname host)) (short (and short-hostname (list (get-short-hostname host)))) (aliases (and aliases (get-hostattrs :aliases host))) (ips (and ips (append (get-hostattrs :ipv6 host) (get-hostattrs :ipv4 host))))) (cons (format nil "~{~A~^,~}" (cons hostname (append aliases short ips additional-names))) (mapcar #'cdr (get-hostattrs 'sshd:host-public-keys host))))) (defprop %update-known-hosts :posix (file host &key short-hostname (aliases t) (ips t) additional-names) (:apply (file:map-remote-file-lines file (lambda (lines) (loop with host = (preprocess-host host) with (identifier . keys) = (get-host-public-keys host :aliases aliases :short-hostname short-hostname :ips ips :additional-names additional-names) and hostname = (get-hostname host) for line in lines for comma = (position #\, line) and space = (position #\Space line) for index = (if comma (min comma space) space) for line-hostname = (subseq line 0 index) and line-key = (subseq line (1+ space)) unless (string= line-hostname hostname) collect line into accum else if (member line-key keys :test #'string=) collect (format nil "~A ~A" identifier line-key) into accum and do (deletef keys line-key :test #'string=) finally (return (nconc accum (loop for key in keys collect (format nil "~A ~A" identifier key)))))))) (:unapply (destructuring-bind (identifier . keys) (get-host-public-keys host :aliases aliases :short-hostname short-hostname :ips ips :additional-names additional-names) (file:lacks-lines file (loop for key in keys collect (format nil "~A ~A" identifier key)))))) (defproplist known-host :posix (host &key short-hostname (aliases t) (ips t) additional-names) "Ensures that the SSH host keys of HOST are stored in ~/.ssh/known_hosts. If SHORT-HOSTNAME, include the part of HOST's hostname before the first dot as one of the hostnames identifying HOST. Removes any other host keys identifying HOST, to simplify refreshing keys." (:desc #?"${(get-hostname host)} is known host to ssh client") (file:directory-exists ".ssh") (%update-known-hosts ".ssh/known_hosts" host :aliases aliases :short-hostname short-hostname :ips ips :additional-names additional-names)) (defproplist system-known-host :posix (host &key short-hostname (aliases t) (ips t) additional-names) "Ensures that SSH host keys of HOST are stored in /etc/ssh/ssh_known_hosts. If SHORT-HOSTNAME, include the part of HOST's hostname before the first dot as one of the hostnames identifying HOST. Removes any other host keys identifying HOST, to simplify refreshing keys." (:desc #?"${(get-hostname host)} is system known host to ssh client") (%update-known-hosts "/etc/ssh/ssh_known_hosts" host :aliases aliases :short-hostname short-hostname :ips ips :additional-names additional-names)) (defproplist parent-is-system-known-host :posix (&key short-hostname (aliases t) (ips t) additional-names) "Ensures that the SSH host keys of the parent host are stored in /etc/ssh/ssh_known_hosts; SHORT-HOSTNAME is as for SSH:SYSTEM-KNOWN-HOST." (:desc "Parent host is system known host to ssh client") (%update-known-hosts "/etc/ssh/ssh_known_hosts" (make-host :hostattrs (get-hostattrs :parent-hostattrs)) :short-hostname short-hostname :aliases aliases :ips ips :additional-names additional-names))