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", diff --git a/Anvil/Tools/Remote.pm b/Anvil/Tools/Remote.pm index cde272962..fe38f8acf 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; @@ -1319,106 +1319,65 @@ 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, + + 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!!") - { - # 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, - }}); - return(0); - } - - # Look for our key - my $key_found = 0; - my $new_authorized_keys_body = ""; - foreach my $line (split/\n/, $old_authorized_keys_body) + $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 }}); + + # 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)) { - $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"; + unlink $wrapper_script; } - - if (not $key_found) + + if ($return_code) { - # 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) + if ($output =~ / All keys were skipped because they already exist on the remote system/s) { - # Success! - $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, priority => "alert", key => "log_0195", variables => { target => $target }}); - return($access); + # 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 { - # Welp, we tried. - $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, priority => "alert", key => "log_0196", variables => { target => $target }}); - return($access); + # 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. + 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); + } } else { diff --git a/Anvil/Tools/Storage.pm b/Anvil/Tools/Storage.pm index 656b805b9..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. @@ -6154,7 +6076,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} : ""; diff --git a/Anvil/Tools/System.pm b/Anvil/Tools/System.pm index 06577cac4..4bbb0cef0 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"; @@ -31,6 +32,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 +53,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 @@ -1082,7 +1087,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 @@ -3221,6 +3226,72 @@ 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; + 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'}; + $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; + 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'}; + $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. @@ -4276,7 +4347,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} : ""; @@ -6245,6 +6316,96 @@ 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 $exe_name = basename($exe); + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { exe_name => $exe_name }}); + + ### NOTE: The first line needs to be the '#!...' line, hence the odd formatting below. + my $timeout = 3600; + 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 +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, + backup => 0, + 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/anvil.spec.in b/anvil.spec.in index c8a546829..8b81b0de8 100644 --- a/anvil.spec.in +++ b/anvil.spec.in @@ -10,6 +10,7 @@ # 2 backslashes to make shell continue the line, and # 1 more backslash to make rpmbuild continue the line. %define coreservices anvil-daemon.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 @@ -289,6 +290,15 @@ setenforce 0 # enable and start required core services on fresh install if [ $1 -eq 1 ] then + # 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 + systemctl enable --now chronyd.service systemctl enable --now %coreservices fi @@ -393,6 +403,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/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/share/words.xml b/share/words.xml index ec14e2f22..6b033d62e 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. + 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. @@ -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.]]> diff --git a/units/Makefile.am b/units/Makefile.am index e0443923f..0c38f2a7a 100644 --- a/units/Makefile.am +++ b/units/Makefile.am @@ -3,6 +3,8 @@ MAINTAINERCLEANFILES = Makefile.in servicedir = $(SYSTEMD_UNIT_DIR) dist_service_DATA = \ 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-monitor-auth-keys.path b/units/anvil-monitor-auth-keys.path new file mode 100644 index 000000000..abd18a114 --- /dev/null +++ b/units/anvil-monitor-auth-keys.path @@ -0,0 +1,11 @@ +[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 +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..801ed9b6e --- /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=append:/var/log/anvil.log +StandardError=append:/var/log/anvil.log + +[Install] +WantedBy=multi-user.target \ No newline at end of file