;;; 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.connection.sudo) (named-readtables:in-readtable :consfigurator) ;; Note that a password needed to sudo is technically not a piece of ;; prerequisite data required by a deployment, because it is not used in ;; deploying properties in the context of a connection chain which has already ;; been fully established. Nevertheless, we can query sources of prerequisite ;; data to obtain passwords by following the conventions for having ;; prerequisite data sources provide them. (defmethod preprocess-connection-args ((type (eql :sudo)) &key from (user "root")) (list :sudo :user user :password (and from (destructuring-bind (user host) (split-string from :separator "@") (get-data-protected-string (strcat "--user-passwd--" host) user))))) ;; With sudo -S, we must ensure that sudo's stdin is a pipe, not a file, ;; because otherwise the program sudo invokes may rewind(stdin) and read the ;; password, intentionally or otherwise. And UIOP:RUN-PROGRAM empties input ;; streams into temporary files, so there is the potential for this to happen ;; when using :SUDO to apply properties to localhost. Other connection types ;; might work similarly. ;; ;; The simplest way to handle this would be to just put 'cat |' at the ;; beginning of the shell command we construct, but that relies on cat(1) not ;; calling rewind(stdin) either. So we write the password input out to a ;; temporary file ourselves, and use cat(1) to concatenate that file with the ;; actual input. (defclass sudo-connection (shell-wrap-connection) ((password-file :initarg :password-file))) (defmethod establish-connection ((type (eql :sudo)) remaining &key user password) (declare (ignore remaining)) (informat 1 "~&Establishing sudo connection to ~A" user) (make-instance 'sudo-connection :connattrs `(:remote-user ,user) :password-file (and password (aprog1 (mktemp) ;; We'll send the password followed by ^M, then the real stdin. Use ;; CODE-CHAR in this way so that we can be sure ASCII ^M is what ;; will get emitted. (write-remote-file it (strcat (unwrap-passphrase password) (string (code-char 13))) :mode #o600))))) (defmethod connection-tear-down :after ((connection sudo-connection)) (when-let ((file (slot-value connection 'password-file))) (delete-remote-trees file))) (defmethod connection-run ((connection sudo-connection) cmd input) (let* ((file (slot-value connection 'password-file)) (user (connection-connattr connection :remote-user)) (prefix (if file (format nil "cat ~A - | sudo -HkS --prompt=\"\"" (sh-escape file)) "sudo -Hkn"))) ;; Wrap in sh -c so that it is more likely we are either asked for a ;; password for all our commands or not asked for one for any. ;; ;; Preserve SSH_AUTH_SOCK for root to enable this sort of workflow: deploy ;; laptop using (:SUDO :SBCL) and then DEFHOST for laptop contains ;; (DEPLOYS ((:SSH :USER "root")) ...) to deploy a VM on the laptop. ;; ;; This only works for sudoing to root because only the superuser can ;; access the socket (and was always able to, so we're not granting new ;; access which may be unwanted). (mrun :may-fail :input input (format nil "~A ~:[~;--preserve-env=SSH_AUTH_SOCK ~]--user=~A sh -c ~A" prefix (string= user "root") user (sh-escape cmd)))))