From 89e9159ff8fcbc822f3c8d21becb2f3017952ebe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lesimple?= Date: Wed, 4 Dec 2024 13:24:27 +0000 Subject: [PATCH 1/2] feat: add assetForgetHostKey --- bin/helper/osh-assetForgetHostKey | 119 ++++++++++++++++++ bin/plugin/restricted/assetForgetHostKey | 43 +++++++ .../plugins/restricted/assetForgetHostKey.rst | 24 ++++ doc/sphinx/plugins/restricted/index.rst | 1 + etc/sudoers.d/osh-plugin-assetForgetHostKey | 2 + lib/perl/OVH/Bastion/allowkeeper.inc | 14 ++- .../tests.d/345-assetforgethostkey.sh | 73 +++++++++++ 7 files changed, 274 insertions(+), 2 deletions(-) create mode 100755 bin/helper/osh-assetForgetHostKey create mode 100755 bin/plugin/restricted/assetForgetHostKey create mode 100644 doc/sphinx/plugins/restricted/assetForgetHostKey.rst create mode 100644 etc/sudoers.d/osh-plugin-assetForgetHostKey create mode 100644 tests/functional/tests.d/345-assetforgethostkey.sh diff --git a/bin/helper/osh-assetForgetHostKey b/bin/helper/osh-assetForgetHostKey new file mode 100755 index 000000000..5dd0fdde2 --- /dev/null +++ b/bin/helper/osh-assetForgetHostKey @@ -0,0 +1,119 @@ +#! /usr/bin/perl -T +# vim: set filetype=perl ts=4 sw=4 sts=4 et: +# NEEDGROUP osh-assetForgetHostKey +# SUDOERS %osh-assetForgetHostKey ALL=(root) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-assetForgetHostKey * +# FILEMODE 0700 +# FILEOWN 0 0 + +#>HEADER +use common::sense; +use Getopt::Long qw(:config no_auto_abbrev no_ignore_case); +use DateTime; + +use File::Basename; +use lib dirname(__FILE__) . '/../../lib/perl'; +use OVH::Result; +use OVH::Bastion; +use OVH::Bastion::Helper; + +# Fetch command options +my $fnret; +my ($result, @optwarns); +my ($ip, $port); +eval { + local $SIG{__WARN__} = sub { push @optwarns, shift }; + $result = GetOptions( + "ip=s" => sub { $ip //= $_[1] }, + "port=i" => sub { $port //= $_[1] }, + ); +}; +if ($@) { die $@ } + +if (!$result) { + local $" = ", "; + HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns"); +} + +OVH::Bastion::Helper::check_spurious_args(); + +if (not $ip or not $port) { + HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'ip' or 'port'"); +} + +#
CODE + +# Build the regex we'll be looking for. +my $re; +if ($port == 22) { + # format is "IP ssh-..." + $re = qr/^\Q$ip ssh-\E/m; +} +else { + # format is "[IP]:port ssh-..." + $re = qr/^\Q[$ip]:$port ssh-\E/m; +} + +# First, get all bastion accounts, including realm sysaccounts +$fnret = OVH::Bastion::get_account_list(); +$fnret or HEXIT($fnret); +my %accounts = %{$fnret->value || {}}; + +$fnret = OVH::Bastion::get_realm_list(); +$fnret or HEXIT($fnret); +foreach my $realmName (keys %{$fnret->value || {}}) { + $accounts{$fnret->value->{$realmName}{'sysaccount'}} = $fnret->value->{$realmName}; +} + +my $nbchanges = 0; +my $now = DateTime->now()->iso8601() . 'Z'; +foreach my $name (keys %accounts) { + my $accountHome = $accounts{$name}{'home'}; + if (!-d $accountHome) { + warn_syslog("Account '$name' home '$accountHome' doesn't exist"); + next; + } + + my $knownHosts = "$accountHome/.ssh/known_hosts"; + if (!-f $knownHosts) { + # This can happen if the account has never been used yet + next; + } + + # now, slurp the file and look for the host we're being asked about + if (open(my $fh, '<', $knownHosts)) { + my $contents = do { + local $/; + <$fh>; + }; + close($fh); + + my $nbmatches = $contents =~ s/$re/# removed by $self at $now in session with uniqid $ENV{'UNIQID'}: $&/g; + + # remove found lines if any + if ($nbmatches) { + osh_info("Removing $nbmatches lines from ${name}'s known_hosts file"); + if (open($fh, '>', $knownHosts)) { + print $fh $contents; + close($fh); + $nbchanges++; + } + else { + osh_warn("Couldn't adjust ${name}'s known_hosts file"); + warn_syslog("Error while opening $knownHosts file for write: $!"); + } + } + } + else { + warn_syslog("Couldn't open '$knownHosts': $!"); + } +} + +HEXIT( + R( + $nbchanges ? 'OK' : 'OK_NO_CHANGE', + msg => "Finally modified $nbchanges known_hosts accounts' files", + value => {changed_files => $nbchanges} + ) +); diff --git a/bin/plugin/restricted/assetForgetHostKey b/bin/plugin/restricted/assetForgetHostKey new file mode 100755 index 000000000..75eab0e72 --- /dev/null +++ b/bin/plugin/restricted/assetForgetHostKey @@ -0,0 +1,43 @@ +#! /usr/bin/env perl +# vim: set filetype=perl ts=4 sw=4 sts=4 et: +use common::sense; + +use File::Basename; +use lib dirname(__FILE__) . '/../../../lib/perl'; +use OVH::Result; +use OVH::Bastion; +use OVH::Bastion::Plugin qw( :DEFAULT help ); + +my $remainingOptions = OVH::Bastion::Plugin::begin( + argv => \@ARGV, + header => "remove the host key of a given asset from all accounts' known hosts", + options => {}, + helptext => <<'EOF', +Remove the host key of a given asset from all accounts' known hosts + +Usage: --osh SCRIPT_NAME --host [--port ] + + --host HOST|IP Asset whose host key should be removed + --port PORT Asset port serving SSH (default: 22) +EOF +); + +if (!$ip) { + help(); + osh_exit 'ERR_MISSING_PARAMETER', "Missing mandatory parameter --host (or host didn't resolve correctly)"; +} + +# IP can't be a prefix +if ($ip =~ m{/}) { + help(); + osh_exit 'ERR_INVALID_PARAMETER', "Specified IP must not be a prefix ($ip)"; +} + +osh_info "Removing $ip host key from accounts..."; + +my @command = qw{ sudo -n -u root -- /usr/bin/env perl -T }; +push @command, $OVH::Bastion::BASEPATH . '/bin/helper/osh-assetForgetHostKey'; +push @command, '--ip', $ip; +push @command, '--port', ($port ? $port : 22); + +osh_exit OVH::Bastion::helper(cmd => \@command); diff --git a/doc/sphinx/plugins/restricted/assetForgetHostKey.rst b/doc/sphinx/plugins/restricted/assetForgetHostKey.rst new file mode 100644 index 000000000..13e426915 --- /dev/null +++ b/doc/sphinx/plugins/restricted/assetForgetHostKey.rst @@ -0,0 +1,24 @@ +=================== +assetForgetHostKey +=================== + +Remove the host key of a given asset from all accounts' known hosts +=================================================================== + + +.. admonition:: usage + :class: cmdusage + + --osh assetForgetHostKey --host [--port ] + +.. program:: assetForgetHostKey + + +.. option:: --host HOST|IP + + Asset whose host key should be removed + +.. option:: --port PORT + + Asset port serving SSH (default: 22) + diff --git a/doc/sphinx/plugins/restricted/index.rst b/doc/sphinx/plugins/restricted/index.rst index 05c091769..ed8efa0ae 100644 --- a/doc/sphinx/plugins/restricted/index.rst +++ b/doc/sphinx/plugins/restricted/index.rst @@ -25,6 +25,7 @@ restricted plugins accountUnexpire accountUnfreeze accountUnlock + assetForgetHostKey groupCreate groupDelete realmCreate diff --git a/etc/sudoers.d/osh-plugin-assetForgetHostKey b/etc/sudoers.d/osh-plugin-assetForgetHostKey new file mode 100644 index 000000000..0f32cc457 --- /dev/null +++ b/etc/sudoers.d/osh-plugin-assetForgetHostKey @@ -0,0 +1,2 @@ +# to modify all accounts' known_hosts we need to be root +%osh-assetForgetHostKey ALL=(root) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-assetForgetHostKey * diff --git a/lib/perl/OVH/Bastion/allowkeeper.inc b/lib/perl/OVH/Bastion/allowkeeper.inc index db4d43718..68339877a 100644 --- a/lib/perl/OVH/Bastion/allowkeeper.inc +++ b/lib/perl/OVH/Bastion/allowkeeper.inc @@ -911,9 +911,19 @@ sub get_realm_list { cache => 1 ); + my $entry = $fnret->value->{$name}; + # add proper realms - $name =~ s{^realm_}{}; - $users{$name} = {name => $name}; + my $realmName = $entry->{'name'}; + $realmName =~ s{^realm_}{}; + $users{$realmName} = { + sysaccount => $name, + name => $realmName, + gid => $entry->{'gid'}, + home => $entry->{'dir'}, + shell => $entry->{'shell'}, + uid => $entry->{'uid'}, + }; } return R('OK', value => \%users); diff --git a/tests/functional/tests.d/345-assetforgethostkey.sh b/tests/functional/tests.d/345-assetforgethostkey.sh new file mode 100644 index 000000000..3aaca3484 --- /dev/null +++ b/tests/functional/tests.d/345-assetforgethostkey.sh @@ -0,0 +1,73 @@ +# vim: set filetype=sh ts=4 sw=4 sts=4 et: +# shellcheck shell=bash +# shellcheck disable=SC2086,SC2016,SC2046 +# below: convoluted way that forces shellcheck to source our caller +# shellcheck source=tests/functional/launch_tests_on_instance.sh +. "$(dirname "${BASH_SOURCE[0]}")"/dummy + +testsuite_assetforgethostkey() +{ + # create a1 and a2 + grant accountCreate + + success create_account1 $a0 --osh accountCreate --account $account1 --uid $uid1 --public-key \""$(cat $account1key1file.pub)"\" + json .error_code OK .command accountCreate .value null + + success create_account2 $a0 --osh accountCreate --account $account2 --uid $uid2 --public-key \""$(cat $account2key1file.pub)"\" + json .error_code OK .command accountCreate .value null + + revoke accountCreate + + # grant personal accesses to these accounts + grant accountAddPersonalAccess + + success a0_allow_a1_localhost $a0 --osh accountAddPersonalAccess --account $account1 --host 127.0.0.0/24 --port '*' --user '*' + json .error_code OK .command accountAddPersonalAccess + + success a0_allow_a2_localhost $a0 --osh accountAddPersonalAccess --account $account2 --host 127.0.0.0/24 --port '*' --user '*' + json .error_code OK .command accountAddPersonalAccess + + revoke accountAddPersonalAccess + + # connect to localhost from these accounts (it won't work in the end but their known_hosts files will be updated and that's what we need) + run a1_connect_localhost1 $a1 user1@127.0.0.1 + contain "Connecting..." + + run a2_connect_localhost1_226 $a2 user1@127.0.0.1 -p 226 + contain "Connecting..." + + run a2_connect_localhost1 $a2 user1@127.0.0.1 + contain "Connecting..." + + run a2_connect_localhost2 $a2 user1@127.0.0.2 + contain "Connecting..." + + grant assetForgetHostKey + + # now, delete the host keys for 127.0.0.1 + success a0_asset_forgethostkey $a0 --osh assetForgetHostKey --host 127.0.0.1 + json .error_code OK .command assetForgetHostKey .value.changed_files 2 + + success a0_asset_forgethostkey_dupe $a0 --osh assetForgetHostKey --host 127.0.0.1 + json .error_code OK_NO_CHANGE .command assetForgetHostKey .value.changed_files 0 + + # same but with port 226 + success a0_asset_forgethostkey_226 $a0 --osh assetForgetHostKey --host 127.0.0.1 --port 226 + json .error_code OK .command assetForgetHostKey .value.changed_files 1 + + success a0_asset_forgethostkey_226_dupe $a0 --osh assetForgetHostKey --host 127.0.0.1 --port 226 + json .error_code OK_NO_CHANGE .command assetForgetHostKey .value.changed_files 0 + + revoke assetForgetHostKey + + # delete those accounts + grant accountDelete + + success account1_cleanup $a0 --osh accountDelete --account $account1 --no-confirm + success account2_cleanup $a0 --osh accountDelete --account $account2 --no-confirm + + revoke accountDelete +} + +testsuite_assetforgethostkey +unset -f testsuite_assetforgethostkey From b3748dacbd6ee03944572ae5421da67bbed4a234 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lesimple?= Date: Wed, 4 Dec 2024 13:25:00 +0000 Subject: [PATCH 2/2] chore: speedup tests in 330-selfkeys.sh --- tests/functional/tests.d/330-selfkeys.sh | 29 +++++++++++------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/tests/functional/tests.d/330-selfkeys.sh b/tests/functional/tests.d/330-selfkeys.sh index 558e4584f..e3d751da6 100644 --- a/tests/functional/tests.d/330-selfkeys.sh +++ b/tests/functional/tests.d/330-selfkeys.sh @@ -11,7 +11,7 @@ _ingress_from_test() { local testname="$1" ip1="$2" ip2="$3" keytoadd="$4" fingerprint="$5" - script $testname "echo '$keytoadd' | $a1 --osh selfAddIngressKey" + script ${testname}_addkey "echo '$keytoadd' | $a1 --osh selfAddIngressKey" retvalshouldbe 0 json .value.connect_only_from[0] $ip1 json .value.connect_only_from[1] $ip2 @@ -23,7 +23,7 @@ _ingress_from_test() json .value.key.prefix "from=\"$ip1,$ip2\"" fi - success $testname $a1 --osh selfListIngressKeys + success ${testname}_listkeys $a1 --osh selfListIngressKeys json .value.keys[1].from_list[0] $ip1 json .value.keys[1].from_list[1] $ip2 if [ "$ip1" = null ] && [ "$ip2" = null ]; then @@ -32,18 +32,13 @@ _ingress_from_test() json .value.keys[1].prefix "from=\"$ip1,$ip2\"" fi - success $testname $a1 --osh selfDelIngressKey -f "$fingerprint" + success ${testname}_delkey $a1 --osh selfDelIngressKey -f "$fingerprint" # now on account creation - grant accountCreate - - script $testname "echo '$keytoadd' | $a0 --osh accountCreate --account $account2 --uid $uid2" + script ${testname}_create_a2 "echo '$keytoadd' | $a0 --osh accountCreate --account $account2 --uid $uid2" json .error_code OK .command accountCreate .value null - revoke accountCreate - grant accountListIngressKeys - - success $testname $a0 --osh accountListIngressKeys --account $account2 + success ${testname}_listkeys_a2 $a0 --osh accountListIngressKeys --account $account2 json .value.keys[0].from_list[0] $ip1 json .value.keys[0].from_list[1] $ip2 if [ "$ip1" = null ] && [ "$ip2" = null ]; then @@ -52,14 +47,10 @@ _ingress_from_test() json .value.keys[0].prefix "from=\"$ip1,$ip2\"" fi - revoke accountListIngressKeys - grant accountDelete - - script $testname "$a0 --osh accountDelete --account $account2" "<<< \"Yes, do as I say and delete $account2, kthxbye\"" + script ${testname}_delete_a2 "$a0 --osh accountDelete --account $account2" "<<< \"Yes, do as I say and delete $account2, kthxbye\"" retvalshouldbe 0 json .error_code OK .command accountDelete - revoke accountDelete } testsuite_selfkeys() @@ -643,6 +634,10 @@ EOS EOS ) + grant accountCreate + grant accountListIngressKeys + grant accountDelete + # ingresskeysfrom=0.0.0.0/0,255.255.255.255, allowoverride=1, noFrom configchg 's=^\\\\x22ingressKeysFromAllowOverride\\\\x22.+=\\\\x22ingressKeysFromAllowOverride\\\\x22:1,=' configchg 's=^\\\\x22ingressKeysFrom\\\\x22:.+=\\\\x22ingressKeysFrom\\\\x22:\\\\x5B\\\\x220.0.0.0/0\\\\x22,\\\\x22255.255.255.255\\\\x22\\\\x5D,=' @@ -673,8 +668,10 @@ EOS # ingresskeysfrom=empty allowoverride=0, withFrom _ingress_from_test fromTest8 null null "from=\"1.2.3.4,5.6.7.8\" $(< $account1key2file.pub)" "$account1key2fp" + revoke accountCreate + revoke accountListIngressKeys + # delete account1 - grant accountDelete script cleanup $a0 --osh accountDelete --account $account1 "<<< \"Yes, do as I say and delete $account1, kthxbye\"" retvalshouldbe 0 revoke accountDelete