From aa477976e73974b4f6e3cfc6ee89115a6ed9bb8a Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Thu, 26 Mar 2026 17:34:39 -0400 Subject: [PATCH 01/16] chore: force debug level to 1 in all code that can modify authorized_keys --- Anvil/Tools/Remote.pm | 2 +- Anvil/Tools/System.pm | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Anvil/Tools/Remote.pm b/Anvil/Tools/Remote.pm index cde272962..7aa029996 100644 --- a/Anvil/Tools/Remote.pm +++ b/Anvil/Tools/Remote.pm @@ -1139,7 +1139,7 @@ sub test_access my $self = shift; my $parameter = shift; my $anvil = $self->parent; - my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; + my $debug = 1; $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "Remote->test_access()" }}); my $close = defined $parameter->{'close'} ? $parameter->{'close'} : 1; diff --git a/Anvil/Tools/System.pm b/Anvil/Tools/System.pm index 06577cac4..c549b90b5 100644 --- a/Anvil/Tools/System.pm +++ b/Anvil/Tools/System.pm @@ -1082,7 +1082,7 @@ sub check_ssh_keys my $self = shift; my $parameter = shift; my $anvil = $self->parent; - my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; + my $debug = 1; $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "System->check_ssh_keys()" }}); # We do a couple things here. First we make sure our user's keys are up to date and stored in the @@ -4276,7 +4276,7 @@ sub manage_authorized_keys my $self = shift; my $parameter = shift; my $anvil = $self->parent; - my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; + my $debug = 1; $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "System->manage_authorized_keys()" }}); my $host_uuid = defined $parameter->{host_uuid} ? $parameter->{host_uuid} : ""; From ea27cbc05575f23abf6b36f71c6daeadae3b384f Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Sat, 28 Mar 2026 00:04:55 -0400 Subject: [PATCH 02/16] chore: use systemd.path and auditd to monitor authorized_keys --- anvil.spec.in | 2 ++ selinux/anvil-subnode.te.in | 9 ++++++++- units/Makefile.am | 3 +++ units/anvil-audit-auth-keys.service | 11 +++++++++++ units/anvil-monitor-auth-keys.path | 9 +++++++++ units/anvil-monitor-auth-keys.service | 14 ++++++++++++++ 6 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 units/anvil-audit-auth-keys.service create mode 100644 units/anvil-monitor-auth-keys.path create mode 100644 units/anvil-monitor-auth-keys.service diff --git a/anvil.spec.in b/anvil.spec.in index c8a546829..c09b8306d 100644 --- a/anvil.spec.in +++ b/anvil.spec.in @@ -10,6 +10,8 @@ # 2 backslashes to make shell continue the line, and # 1 more backslash to make rpmbuild continue the line. %define coreservices anvil-daemon.service \\\ +anvil-audit-auth-keys.service \\\ +anvil-monitor-auth-keys.path \\\ anvil-monitor-network.service \\\ scancore.service ### This adds a lot of noise to anvil.log and likely only useful in rare debug diff --git a/selinux/anvil-subnode.te.in b/selinux/anvil-subnode.te.in index 95f21fad1..5925fba5c 100644 --- a/selinux/anvil-subnode.te.in +++ b/selinux/anvil-subnode.te.in @@ -14,9 +14,11 @@ policy_module(anvil-subnode, 1.1.0) # Use existing types; don't declare unless it's new. # require { + type init_t; type mnt_t; - type sysctl_vm_t; + type ssh_home_t; type svirt_t; + type sysctl_vm_t; type virsh_t; class file { getattr open read }; } @@ -26,6 +28,11 @@ require { # drbd rules will be provided by drbd-utils package. +#============= init_t ============== +# allow systemd to monitor /root/.ssh/authorized_keys with path unit +allow init_t ssh_home_t:file { read }; + + #============= virsh_t ============== # Needed for virsh to access the domain XMLs under /mnt. allow virsh_t mnt_t:file { open read }; diff --git a/units/Makefile.am b/units/Makefile.am index e0443923f..32a869399 100644 --- a/units/Makefile.am +++ b/units/Makefile.am @@ -2,7 +2,10 @@ MAINTAINERCLEANFILES = Makefile.in servicedir = $(SYSTEMD_UNIT_DIR) dist_service_DATA = \ + anvil-audit-auth-keys.service \ anvil-daemon.service \ + anvil-monitor-auth-keys.path \ + anvil-monitor-auth-keys.service \ anvil-monitor-daemons.service \ anvil-monitor-drbd.service \ anvil-monitor-lvm.service \ diff --git a/units/anvil-audit-auth-keys.service b/units/anvil-audit-auth-keys.service new file mode 100644 index 000000000..73bc72994 --- /dev/null +++ b/units/anvil-audit-auth-keys.service @@ -0,0 +1,11 @@ +[Unit] +Description=Configures auditd to watch /root/.ssh/authorized_keys + +[Service] +Type=exec +ExecStart=/usr/sbin/auditctl -a always,exit -F arch=b64 -F path=/root/.ssh/authorized_keys -F perm=wa +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/units/anvil-monitor-auth-keys.path b/units/anvil-monitor-auth-keys.path new file mode 100644 index 000000000..5ffe79dc4 --- /dev/null +++ b/units/anvil-monitor-auth-keys.path @@ -0,0 +1,9 @@ +[Unit] +Description=Triggers a service to collect info on /root/.ssh/authorized_keys when it's modified + +[Path] +PathModified=/root/.ssh/authorized_keys +TriggerLimitBurst=0 + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/units/anvil-monitor-auth-keys.service b/units/anvil-monitor-auth-keys.service new file mode 100644 index 000000000..351ee5011 --- /dev/null +++ b/units/anvil-monitor-auth-keys.service @@ -0,0 +1,14 @@ +[Unit] +Description=Collects info on /root/.ssh/authorized_keys + +[Service] +Type=oneshot +ExecStart=/usr/bin/stat /root/.ssh/authorized_keys +ExecStart=/usr/bin/echo "----- START BODY -----" +ExecStart=/usr/bin/cat /root/.ssh/authorized_keys +ExecStart=/usr/bin/echo "----- END BODY -----" +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target \ No newline at end of file From d717cc9bb31d3b4e285339b6ecdd417d3c6d59a8 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Sat, 28 Mar 2026 14:28:49 -0400 Subject: [PATCH 03/16] chore: add auditd dependency to audit-auth-keys.service --- units/anvil-audit-auth-keys.service | 2 ++ 1 file changed, 2 insertions(+) diff --git a/units/anvil-audit-auth-keys.service b/units/anvil-audit-auth-keys.service index 73bc72994..8b8d434fd 100644 --- a/units/anvil-audit-auth-keys.service +++ b/units/anvil-audit-auth-keys.service @@ -1,5 +1,7 @@ [Unit] Description=Configures auditd to watch /root/.ssh/authorized_keys +Requires=auditd.service +After=auditd.service [Service] Type=exec From 98ef2903833840b38ecd696cf8ffd8c5fe6cb89e Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Mon, 30 Mar 2026 16:21:00 -0400 Subject: [PATCH 04/16] chore: make auditd rule permanent, run systemd.path earlier --- anvil.spec.in | 20 ++++++++++++++++++++ units/Makefile.am | 1 - units/anvil-audit-auth-keys.service | 13 ------------- units/anvil-monitor-auth-keys.path | 2 ++ units/anvil-monitor-auth-keys.service | 4 ++-- 5 files changed, 24 insertions(+), 16 deletions(-) delete mode 100644 units/anvil-audit-auth-keys.service diff --git a/anvil.spec.in b/anvil.spec.in index c09b8306d..0d26e1e50 100644 --- a/anvil.spec.in +++ b/anvil.spec.in @@ -293,6 +293,15 @@ if [ $1 -eq 1 ] then systemctl enable --now chronyd.service systemctl enable --now %coreservices + + # write a permanent rule in audit.rules to monitor /root/.ssh/authorized_keys + if [ ! -e '/etc/audit/rules.d/anvil-monitor-auth-keys.rules' ] + then + echo "-a always,exit -F arch=b64 -F path=/root/.ssh/authorized_keys -F perm=wa" >/etc/audit/rules.d/anvil-monitor-auth-keys.rules + chmod 0600 /etc/audit/rules.d/anvil-monitor-auth-keys.rules + restorecon /etc/audit/rules.d/anvil-monitor-auth-keys.rules + augenrules --load + fi fi @@ -395,6 +404,17 @@ touch /etc/anvil/type.dr # Only uninstall our services %systemd_preun %coreservices +# on uninstall (not upgrade)... +if [ $1 -eq 0 ] +then + # remove the rule that monitors /root/.ssh/authorized_keys + if [ -e '/etc/audit/rules.d/anvil-monitor-auth-keys.rules' ] + then + rm -f /etc/audit/rules.d/anvil-monitor-auth-keys.rules + augenrules --load + fi +fi + ### Remove stuff - Disabled for now, messes things up during upgrades %postun core diff --git a/units/Makefile.am b/units/Makefile.am index 32a869399..0c38f2a7a 100644 --- a/units/Makefile.am +++ b/units/Makefile.am @@ -2,7 +2,6 @@ MAINTAINERCLEANFILES = Makefile.in servicedir = $(SYSTEMD_UNIT_DIR) dist_service_DATA = \ - anvil-audit-auth-keys.service \ anvil-daemon.service \ anvil-monitor-auth-keys.path \ anvil-monitor-auth-keys.service \ diff --git a/units/anvil-audit-auth-keys.service b/units/anvil-audit-auth-keys.service deleted file mode 100644 index 8b8d434fd..000000000 --- a/units/anvil-audit-auth-keys.service +++ /dev/null @@ -1,13 +0,0 @@ -[Unit] -Description=Configures auditd to watch /root/.ssh/authorized_keys -Requires=auditd.service -After=auditd.service - -[Service] -Type=exec -ExecStart=/usr/sbin/auditctl -a always,exit -F arch=b64 -F path=/root/.ssh/authorized_keys -F perm=wa -StandardOutput=journal -StandardError=journal - -[Install] -WantedBy=multi-user.target \ No newline at end of file diff --git a/units/anvil-monitor-auth-keys.path b/units/anvil-monitor-auth-keys.path index 5ffe79dc4..abd18a114 100644 --- a/units/anvil-monitor-auth-keys.path +++ b/units/anvil-monitor-auth-keys.path @@ -1,5 +1,7 @@ [Unit] Description=Triggers a service to collect info on /root/.ssh/authorized_keys when it's modified +After=local-fs.target +Before=sysinit.target [Path] PathModified=/root/.ssh/authorized_keys diff --git a/units/anvil-monitor-auth-keys.service b/units/anvil-monitor-auth-keys.service index 351ee5011..64f2c0128 100644 --- a/units/anvil-monitor-auth-keys.service +++ b/units/anvil-monitor-auth-keys.service @@ -7,8 +7,8 @@ ExecStart=/usr/bin/stat /root/.ssh/authorized_keys ExecStart=/usr/bin/echo "----- START BODY -----" ExecStart=/usr/bin/cat /root/.ssh/authorized_keys ExecStart=/usr/bin/echo "----- END BODY -----" -StandardOutput=journal -StandardError=journal +StandardOutput=file:/var/log/anvil.log +StandardError=file:/var/log/anvil.log [Install] WantedBy=multi-user.target \ No newline at end of file From 16215e36970760cc78d579356e46d374650701df Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Mon, 30 Mar 2026 17:46:14 -0400 Subject: [PATCH 05/16] chore: remove unused audit-auth-keys.service, set auditd rule before starting core services --- anvil.spec.in | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/anvil.spec.in b/anvil.spec.in index 0d26e1e50..8b81b0de8 100644 --- a/anvil.spec.in +++ b/anvil.spec.in @@ -10,7 +10,6 @@ # 2 backslashes to make shell continue the line, and # 1 more backslash to make rpmbuild continue the line. %define coreservices anvil-daemon.service \\\ -anvil-audit-auth-keys.service \\\ anvil-monitor-auth-keys.path \\\ anvil-monitor-network.service \\\ scancore.service @@ -291,9 +290,6 @@ setenforce 0 # enable and start required core services on fresh install if [ $1 -eq 1 ] then - systemctl enable --now chronyd.service - systemctl enable --now %coreservices - # write a permanent rule in audit.rules to monitor /root/.ssh/authorized_keys if [ ! -e '/etc/audit/rules.d/anvil-monitor-auth-keys.rules' ] then @@ -302,6 +298,9 @@ then restorecon /etc/audit/rules.d/anvil-monitor-auth-keys.rules augenrules --load fi + + systemctl enable --now chronyd.service + systemctl enable --now %coreservices fi From 3cbb6ad071dc8e3ce11de5e9326f5f8d213f792d Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 31 Mar 2026 16:42:03 -0400 Subject: [PATCH 06/16] chore: try append to anvil.log in monitor-auth-keys service --- units/anvil-monitor-auth-keys.service | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/units/anvil-monitor-auth-keys.service b/units/anvil-monitor-auth-keys.service index 64f2c0128..801ed9b6e 100644 --- a/units/anvil-monitor-auth-keys.service +++ b/units/anvil-monitor-auth-keys.service @@ -7,8 +7,8 @@ ExecStart=/usr/bin/stat /root/.ssh/authorized_keys ExecStart=/usr/bin/echo "----- START BODY -----" ExecStart=/usr/bin/cat /root/.ssh/authorized_keys ExecStart=/usr/bin/echo "----- END BODY -----" -StandardOutput=file:/var/log/anvil.log -StandardError=file:/var/log/anvil.log +StandardOutput=append:/var/log/anvil.log +StandardError=append:/var/log/anvil.log [Install] WantedBy=multi-user.target \ No newline at end of file From b8b1aad4905bea07a8264725438072bdc69c3877 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 31 Mar 2026 17:06:24 -0400 Subject: [PATCH 07/16] fix: correct entry log in Storage->_wait_if_changing --- Anvil/Tools/Storage.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Anvil/Tools/Storage.pm b/Anvil/Tools/Storage.pm index 656b805b9..7a8aa2e1c 100644 --- a/Anvil/Tools/Storage.pm +++ b/Anvil/Tools/Storage.pm @@ -6154,7 +6154,7 @@ sub _wait_if_changing my $parameter = shift; my $anvil = $self->parent; my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; - $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "Storage->_create_rsync_wrapper()" }}); + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "Storage->_wait_if_changing()" }}); # Check my parameters. my $file = defined $parameter->{file} ? $parameter->{file} : ""; From 4bf92d5ab78b27b162144f97be708b53b5880dfb Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 31 Mar 2026 19:35:48 -0400 Subject: [PATCH 08/16] fix: add paths to ssh-copy-id and mktemp --- Anvil/Tools.pm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Anvil/Tools.pm b/Anvil/Tools.pm index a8ef4275a..7333121da 100644 --- a/Anvil/Tools.pm +++ b/Anvil/Tools.pm @@ -1318,6 +1318,7 @@ sub _set_paths man => "/usr/bin/man", md5sum => "/usr/bin/md5sum", 'mkdir' => "/usr/bin/mkdir", + 'mktemp' => "/usr/bin/mktemp", modifyrepo_c => "/usr/bin/modifyrepo_c", modprobe => "/usr/sbin/modprobe", mv => "/usr/bin/mv", @@ -1367,6 +1368,7 @@ sub _set_paths snmpset => "/usr/bin/snmpset", 'sort' => "/usr/bin/sort", ss => "/usr/sbin/ss", + 'ssh-copy-id' => "/usr/bin/ssh-copy-id", 'ssh-keygen' => "/usr/bin/ssh-keygen", 'ssh-keyscan' => "/usr/bin/ssh-keyscan", 'stat' => "/usr/bin/stat", From f9707bf8d8cf19e84f563328d14c12241f9e3fa5 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 31 Mar 2026 19:39:27 -0400 Subject: [PATCH 09/16] refactor: rewrite Storage->_create_rsync_wrapper in System.pm --- Anvil/Tools/Storage.pm | 80 +------------------- Anvil/Tools/System.pm | 168 +++++++++++++++++++++++++++++++++++++++++ share/words.xml | 4 +- 3 files changed, 171 insertions(+), 81 deletions(-) diff --git a/Anvil/Tools/Storage.pm b/Anvil/Tools/Storage.pm index 7a8aa2e1c..cdbfa2327 100644 --- a/Anvil/Tools/Storage.pm +++ b/Anvil/Tools/Storage.pm @@ -48,7 +48,6 @@ my $THIS_FILE = "Storage.pm"; # update_config # update_file # write_file -# _create_rsync_wrapper # _wait_if_changing =pod @@ -4882,7 +4881,7 @@ sub rsync if ($password) { # Remote target, wrapper needed. - $wrapper_script = $anvil->Storage->_create_rsync_wrapper({ + $wrapper_script = $anvil->System->create_rsync_wrapper({ debug => $debug, target => $target, password => $password, @@ -6052,83 +6051,6 @@ fi"; ############################################################################################################# -=head2 _create_rsync_wrapper - -This does the actual work of creating the C<< expect >> wrapper script and returns the path to that wrapper for C<< rsync >> calls. - -If there is a problem, an empty string will be returned. - -Parameters; - -=head3 target (required) - -This is the IP address or (resolvable) host name of the remote machine. - -=head3 password (required) - -This is the password of the user you will be connecting to the remote machine as. - -=cut -sub _create_rsync_wrapper -{ - my $self = shift; - my $parameter = shift; - my $anvil = $self->parent; - my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; - $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "Storage->_create_rsync_wrapper()" }}); - - # Check my parameters. - my $target = defined $parameter->{target} ? $parameter->{target} : ""; - my $password = defined $parameter->{password} ? $parameter->{password} : ""; - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { - password => $anvil->Log->is_secure($password), - target => $target, - }}); - - if (not $target) - { - $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Storage->_create_rsync_wrapper()", parameter => "target" }}); - return(""); - } - if (not $password) - { - $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Storage->_create_rsync_wrapper()", parameter => "password" }}); - return(""); - } - - ### NOTE: The first line needs to be the '#!...' line, hence the odd formatting below. - my $timeout = 3600; - my $wrapper_script = "/tmp/rsync.$target"; - my $wrapper_body = "#!".$anvil->data->{path}{exe}{expect}." -set timeout ".$timeout." -eval spawn rsync \$argv -expect \"password:\" \{ send \"".$password."\\n\" \} -expect eof -"; - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { - wrapper_script => $wrapper_script, - wrapper_body => $wrapper_body, - }}); - $anvil->Storage->write_file({ - debug => $debug, - body => $wrapper_body, - file => $wrapper_script, - mode => "0700", - overwrite => 1, - secure => 1, - }); - - if (not -e $wrapper_script) - { - # Failed! - $wrapper_script = ""; - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 0, list => { wrapper_script => $wrapper_script }}); - } - - return($wrapper_script); -} - - =head3 _wait_if_changing This takes a full path to a file, and watches it for at specified number of seconds to see if the size is changing. If it is, this method waits until the file size stops changing. diff --git a/Anvil/Tools/System.pm b/Anvil/Tools/System.pm index c549b90b5..8159dcbd9 100644 --- a/Anvil/Tools/System.pm +++ b/Anvil/Tools/System.pm @@ -31,6 +31,8 @@ my $THIS_FILE = "System.pm"; # check_storage # collect_ipmi_data # configure_ipmi +# create_rsync_wrapper +# create_ssh_copy_id_wrapper # disable_daemon # enable_daemon # find_matching_ip @@ -50,7 +52,9 @@ my $THIS_FILE = "System.pm"; # stty_echo # update_hosts # wait_on_dnf +# _abort_if_ci # _check_anvil_conf +# _create_wrapper_with_expect # _load_firewalld_zones # _load_specific_firewalld_zone # _match_port_to_service @@ -3221,6 +3225,70 @@ LIMIT 1 return($host_ipmi); } +=head2 create_rsync_wrapper + +This creates the C<< expect >> wrapper script and returns the path to that wrapper for C<< rsync >> calls. + +See System->_create_wrapper_with_expect() for details. + +Parameters; + +=head3 password (required) + +This is the password of the user you will be connecting to the remote machine as. + +=head3 target (optional) + +This is the IP address or (resolvable) host name of the remote machine. + +This is suppied to C<< id >> in System->_create_wrapper_with_expect(). + +=cut +sub create_rsync_wrapper +{ + my $self = shift; + my $parameter = shift; + my $anvil = $self->parent; + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "System->create_rsync_wrapper()" }}); + + $parameter->{exe} = $anvil->data->{path}{exe}{'rsync'}; + $parameter->{id} = $parameter->{target}; + + return $anvil->System->_create_wrapper_with_expect($parameter); +} + +=head2 create_ssh_copy_id_wrapper + +This creates the C<< expect >> wrapper script and returns the path to that wrapper for C<< ssh-copy-id >> calls. + +See System->_create_wrapper_with_expect() for details. + +Parameters; + +=head3 password (required) + +This is the password of the user you will be connecting to the remote machine as. + +=head3 target (optional) + +This is the IP address or (resolvable) host name of the remote machine. + +This is suppied to C<< id >> in System->_create_wrapper_with_expect(). + +=cut +sub create_ssh_copy_id_wrapper +{ + my $self = shift; + my $parameter = shift; + my $anvil = $self->parent; + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "System->create_ssh_copy_id_wrapper()" }}); + + $parameter->{exe} = $anvil->data->{path}{exe}{'ssh-copy-id'}; + $parameter->{id} = $parameter->{target}; + + return $anvil->System->_create_wrapper_with_expect($parameter); +} + =head2 disable_daemon This method disables a daemon. The return code from the disable request will be returned. @@ -6245,6 +6313,106 @@ sub _check_anvil_conf return(0); } +=head2 _create_wrapper_with_expect + +This does the actual work of creating the C<< expect >> wrapper script and returns the path to that wrapper for C<< exe >> calls. + +If there is a problem, an empty string will be returned. + +This should NOT be called directly, use it with something like create_rsync_wrapper or create_ssh_copy_id_wrapper + +Parameters; + +=head3 exe (required) + +This is the executable to wrap with expect. + +=head3 password (required) + +This is the password to supply to the C<< exe >>. + +=head3 id (optional) + +This is the wrapper's readable identifier. + +For example, it can be the IP address or (resolvable) host name of the remote machine when creating a rsync wrapper. + +=cut +sub _create_wrapper_with_expect +{ + my $self = shift; + my $parameter = shift; + my $anvil = $self->parent; + my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "System->_create_wrapper_with_expect()" }}); + + # Check my parameters. + my $exe = defined $parameter->{exe} ? $parameter->{exe} : ""; + my $id = defined $parameter->{id} ? $parameter->{id} : ""; + my $password = defined $parameter->{password} ? $parameter->{password} : ""; + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { + exe => $exe, + id => $id, + password => $anvil->Log->is_secure($password), + }}); + + if (not $exe) + { + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "System->_create_wrapper_with_expect()", parameter => "target" }}); + return(""); + } + if (not $password) + { + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "System->_create_wrapper_with_expect()", parameter => "password" }}); + return(""); + } + + my $shell_call = $anvil->data->{path}{exe}{mktemp}." --tmpdir=".$anvil->data->{path}{directories}{tmp}." ".$exe.".".$id.".XXXXXX"; + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { shell_call => $shell_call }}); + + my ($output, $return_code) = $anvil->System->call({shell_call => $shell_call}); + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { output => $output, return_code => $return_code }}); + + if ($return_code) + { + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 0, list => { shell_call => $shell_call, output => $output, return_code => $return_code }}); + return(""); + } + + chomp($output); + + ### NOTE: The first line needs to be the '#!...' line, hence the odd formatting below. + my $timeout = 3600; + my $wrapper_script = $output; + my $wrapper_body = "#!".$anvil->data->{path}{exe}{expect}." +set timeout ".$timeout." +eval spawn ".$exe." \$argv +expect \"password:\" \{ send \"".$password."\\n\" \} +expect eof +"; + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { + wrapper_script => $wrapper_script, + wrapper_body => $wrapper_body, + }}); + $anvil->Storage->write_file({ + debug => $debug, + body => $wrapper_body, + file => $wrapper_script, + mode => "0700", + overwrite => 1, + secure => 1, + }); + + if (not -e $wrapper_script) + { + # Failed! + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 0, list => { wrapper_script => $wrapper_script }}); + return(""); + } + + return($wrapper_script); +} + =head2 _load_firewalld_zones This reads in the XML files for all of the firewalld zones. diff --git a/share/words.xml b/share/words.xml index ec14e2f22..025fc2235 100644 --- a/share/words.xml +++ b/share/words.xml @@ -2090,7 +2090,7 @@ The database connection error was: - The password: [#!variable!password!#] was successfully used to connect to the host: [#!variable!target!#], however the target user's authorized keys file: [#!variable!file!#] was not read. As such, we can not setup passwordless SSH. Given the initial connection test was without password, we will return '0' to indicate no access. + #!free!# No cookies were read, the use is not logged in. The user's UUID: [#!variable!uuid!#] was read, but it didn't match any known users. The user has been logged out. @@ -2108,7 +2108,7 @@ The database connection error was: Connection only to: [#!variable!db_uuid!#], skipping: [#!variable!uuid!#]. The connection to the database: [#!variable!server!#] has failed. Will attempt to reconnect to the database(s). The public RSA key appears to have not been read properly. Read the key: [#!variable!key!#] from the file: [#!variable!file!#]. Expected the key to start with 'ssh-rsa '. - It looks like the attempt to add our RSA key to: [#!variable!file!#] on: [#!variable!target!#] failed. Returning '0' as we failed to connect using no password, as originally called. + It looks like the attempt to add our RSA key to: [#!variable!target!#] failed. Returning '0' as we failed to connect using no password, as originally called. Successfully connected to: [#!variable!target!#] without a password! Despite adding our key, we seemed to have failed to connect to: [#!variable!target!#] without a password, as originall requested. maintenance_mode() was passed an invalid 'set' value: [#!variable!set!#]. No action taken.]]> From 882ed6ef8c9088ab1e54e4799443915b28f7b7aa Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 31 Mar 2026 19:40:47 -0400 Subject: [PATCH 10/16] fix: use ssh-copy-id to setup passwordless ssh in Remote->test_access --- Anvil/Tools/Remote.pm | 128 ++++++++++++------------------------------ 1 file changed, 37 insertions(+), 91 deletions(-) diff --git a/Anvil/Tools/Remote.pm b/Anvil/Tools/Remote.pm index 7aa029996..50eb8e192 100644 --- a/Anvil/Tools/Remote.pm +++ b/Anvil/Tools/Remote.pm @@ -1319,105 +1319,51 @@ sub test_access }}); return(0); } - - # Read the target's authorized_keys file. - my $target_authorized_keys_file = "/root/.ssh/authorized_keys"; - if ($user ne "root") - { - $target_authorized_keys_file = "/home/".$user."/.ssh/authorized_keys"; - } - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { target_authorized_keys_file => $target_authorized_keys_file }}); - - my $old_authorized_keys_body = $anvil->Storage->read_file({ - debug => $debug, - file => $target_authorized_keys_file, - force_read => 1, - port => $port, - password => $this_password, - remote_user => $user, - target => $target, + + # Copy the pub key to the target + my $wrapper_script = $anvil->System->create_ssh_copy_id_wrapper({ + debug => $debug, + target => $target, + password => $this_password, }); - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { old_authorized_keys_body => $old_authorized_keys_body }}); - if ($old_authorized_keys_body eq "!!error!!") + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { wrapper_script => $wrapper_script }}); + + my $shell_call = $wrapper_script." -i ".$public_key_file." -p ".$port." ".$user."@".$target; + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { shell_call => $shell_call }}); + + my ($output, $return_code) = $anvil->System->call({secure => 1, shell_call => $shell_call}); + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, secure => 1, list => { output => $output, return_code => $return_code }}); + + if ($return_code) { - # Failed to read. - $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, priority => "alert", key => "log_0176", variables => { - target => $target, - password => $anvil->Log->is_secure($this_password), - file => $public_key_file, - }}); + # Failed. + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, priority => "alert", key => "log_0194", variables => { target => $target }}); return(0); } + + # Try to connect again, without a password this time. + my $access = $anvil->Remote->test_access({ + debug => $debug, + 'close' => $close, + password => "", + port => $port, + target => $target, + user => $user, + }); + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { access => $access }}); - # Look for our key - my $key_found = 0; - my $new_authorized_keys_body = ""; - foreach my $line (split/\n/, $old_authorized_keys_body) + # Did we get access? + if ($access) { - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { line => $line }}); - if ($line eq $rsa_key) - { - $key_found = 1; - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { key_found => $key_found }}); - last; - } - $new_authorized_keys_body .= $line."\n"; + # Success! + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, priority => "alert", key => "log_0195", variables => { target => $target }}); + return($access); } - - if (not $key_found) + else { - # Append our key. - $new_authorized_keys_body .= $rsa_key."\n"; - - # Write out the new file. - my $problem = $anvil->Storage->write_file({ - debug => $debug, - backup => 1, - file => $target_authorized_keys_file, - body => $new_authorized_keys_body, - group => $user, - mode => "0644", - overwrite => 1, - port => $port, - password => $this_password, - target => $target, - user => $user, - remote_user => $user, - }); - if ($problem) - { - # Failed. - $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, priority => "alert", key => "log_0194", variables => { - target => $target, - file => $target_authorized_keys_file, - }}); - return(0); - } - - # Try to connect again, without a password this time. - my $access = $anvil->Remote->test_access({ - debug => $debug, - 'close' => $close, - password => "", - port => $port, - target => $target, - user => $user, - }); - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { access => $access }}); - - # Did we get access? - if ($access) - { - # Success! - $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, priority => "alert", key => "log_0195", variables => { target => $target }}); - return($access); - } - else - { - # Welp, we tried. - $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, priority => "alert", key => "log_0196", variables => { target => $target }}); - return($access); - } + # Welp, we tried. + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, priority => "alert", key => "log_0196", variables => { target => $target }}); + return($access); } } else From cf55f5015941114323be1ff1495f73793b3a8258 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 31 Mar 2026 20:06:44 -0400 Subject: [PATCH 11/16] fix: add missing debug var in create_*_wrapper --- Anvil/Tools/System.pm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Anvil/Tools/System.pm b/Anvil/Tools/System.pm index 8159dcbd9..028a37730 100644 --- a/Anvil/Tools/System.pm +++ b/Anvil/Tools/System.pm @@ -3249,6 +3249,7 @@ sub create_rsync_wrapper my $self = shift; my $parameter = shift; my $anvil = $self->parent; + my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "System->create_rsync_wrapper()" }}); $parameter->{exe} = $anvil->data->{path}{exe}{'rsync'}; @@ -3281,6 +3282,7 @@ sub create_ssh_copy_id_wrapper my $self = shift; my $parameter = shift; my $anvil = $self->parent; + my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "System->create_ssh_copy_id_wrapper()" }}); $parameter->{exe} = $anvil->data->{path}{exe}{'ssh-copy-id'}; From a54ce0540ecc9dab3f4908553119a0c3239608fa Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 31 Mar 2026 23:09:22 -0400 Subject: [PATCH 12/16] fix: correct mktemp template in System->_create_wrapper_with_expect --- Anvil/Tools/System.pm | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Anvil/Tools/System.pm b/Anvil/Tools/System.pm index 028a37730..ad517d415 100644 --- a/Anvil/Tools/System.pm +++ b/Anvil/Tools/System.pm @@ -14,6 +14,7 @@ use JSON; use Text::Diff; use String::ShellQuote; use Encode; +use File::Basename; our $VERSION = "3.0.0"; my $THIS_FILE = "System.pm"; @@ -6369,7 +6370,10 @@ sub _create_wrapper_with_expect return(""); } - my $shell_call = $anvil->data->{path}{exe}{mktemp}." --tmpdir=".$anvil->data->{path}{directories}{tmp}." ".$exe.".".$id.".XXXXXX"; + my $exe_name = basename($exe); + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { exe_name => $exe_name }}); + + my $shell_call = $anvil->data->{path}{exe}{mktemp}." --tmpdir=".$anvil->data->{path}{directories}{tmp}." ".$exe_name.".".$id.".XXXXXX"; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { shell_call => $shell_call }}); my ($output, $return_code) = $anvil->System->call({shell_call => $shell_call}); From f820443c35a2e1cad4a4a0f9bf8153f38315a1b0 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 31 Mar 2026 23:31:20 -0400 Subject: [PATCH 13/16] fix: remove ssh-copy-id wrapper after use --- Anvil/Tools/Remote.pm | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Anvil/Tools/Remote.pm b/Anvil/Tools/Remote.pm index 50eb8e192..8ef54a419 100644 --- a/Anvil/Tools/Remote.pm +++ b/Anvil/Tools/Remote.pm @@ -1320,7 +1320,6 @@ sub test_access return(0); } - # Copy the pub key to the target my $wrapper_script = $anvil->System->create_ssh_copy_id_wrapper({ debug => $debug, target => $target, @@ -1331,9 +1330,15 @@ sub test_access my $shell_call = $wrapper_script." -i ".$public_key_file." -p ".$port." ".$user."@".$target; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { shell_call => $shell_call }}); + # Copy the public key to the target my ($output, $return_code) = $anvil->System->call({secure => 1, shell_call => $shell_call}); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, secure => 1, list => { output => $output, return_code => $return_code }}); + if (($wrapper_script) && (-e $wrapper_script)) + { + unlink $wrapper_script; + } + if ($return_code) { # Failed. From 525b87f87840c08a713f18cb3560ecab3fdb4316 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 31 Mar 2026 23:55:33 -0400 Subject: [PATCH 14/16] fix: remove mktemp call before making expect script --- Anvil/Tools/System.pm | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/Anvil/Tools/System.pm b/Anvil/Tools/System.pm index ad517d415..aaee7fddd 100644 --- a/Anvil/Tools/System.pm +++ b/Anvil/Tools/System.pm @@ -6370,26 +6370,12 @@ sub _create_wrapper_with_expect return(""); } - my $exe_name = basename($exe); + my $exe_name = basename($exe); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { exe_name => $exe_name }}); - my $shell_call = $anvil->data->{path}{exe}{mktemp}." --tmpdir=".$anvil->data->{path}{directories}{tmp}." ".$exe_name.".".$id.".XXXXXX"; - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { shell_call => $shell_call }}); - - my ($output, $return_code) = $anvil->System->call({shell_call => $shell_call}); - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { output => $output, return_code => $return_code }}); - - if ($return_code) - { - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 0, list => { shell_call => $shell_call, output => $output, return_code => $return_code }}); - return(""); - } - - chomp($output); - ### NOTE: The first line needs to be the '#!...' line, hence the odd formatting below. my $timeout = 3600; - my $wrapper_script = $output; + my $wrapper_script = $anvil->data->{path}{directories}{tmp}."/".$exe_name.".".$id; my $wrapper_body = "#!".$anvil->data->{path}{exe}{expect}." set timeout ".$timeout." eval spawn ".$exe." \$argv From 0f37b7cc54cf2e6e7a1eabdc8d520a1056567064 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 1 Apr 2026 02:26:13 -0400 Subject: [PATCH 15/16] fix: test passwordless ssh if pubkey already exists --- Anvil/Tools/Remote.pm | 14 +++++++++++--- share/words.xml | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Anvil/Tools/Remote.pm b/Anvil/Tools/Remote.pm index 8ef54a419..fe38f8acf 100644 --- a/Anvil/Tools/Remote.pm +++ b/Anvil/Tools/Remote.pm @@ -1341,9 +1341,17 @@ sub test_access if ($return_code) { - # Failed. - $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, priority => "alert", key => "log_0194", variables => { target => $target }}); - return(0); + if ($output =~ / All keys were skipped because they already exist on the remote system/s) + { + # Continue to retry because the key exists on the target. + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, priority => "alert", key => "log_0176", variables => { target => $target }}); + } + else + { + # Log and stop here because adding the key failed with a different reason. + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, priority => "alert", key => "log_0194", variables => { target => $target }}); + return(0); + } } # Try to connect again, without a password this time. diff --git a/share/words.xml b/share/words.xml index 025fc2235..6b033d62e 100644 --- a/share/words.xml +++ b/share/words.xml @@ -2090,7 +2090,7 @@ The database connection error was: - #!free!# + It looks like the attempt to add our RSA key to: [#!variable!target!#] failed because the key already exists. We'll try to connect again. No cookies were read, the use is not logged in. The user's UUID: [#!variable!uuid!#] was read, but it didn't match any known users. The user has been logged out. From 459b058334660b92617fc55dfadcbf6825d51b65 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 1 Apr 2026 15:34:44 -0400 Subject: [PATCH 16/16] fix: don't backup expect scripts --- Anvil/Tools/System.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/Anvil/Tools/System.pm b/Anvil/Tools/System.pm index aaee7fddd..4bbb0cef0 100644 --- a/Anvil/Tools/System.pm +++ b/Anvil/Tools/System.pm @@ -6388,6 +6388,7 @@ expect eof }}); $anvil->Storage->write_file({ debug => $debug, + backup => 0, body => $wrapper_body, file => $wrapper_script, mode => "0700",