Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add assetForgetHostKey #508

Merged
merged 2 commits into from
Dec 10, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions bin/helper/osh-assetForgetHostKey
Original file line number Diff line number Diff line change
@@ -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'");
}

#<HEADER

#>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}
)
);
43 changes: 43 additions & 0 deletions bin/plugin/restricted/assetForgetHostKey
Original file line number Diff line number Diff line change
@@ -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 <HOST|IP> [--port <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);
24 changes: 24 additions & 0 deletions doc/sphinx/plugins/restricted/assetForgetHostKey.rst
Original file line number Diff line number Diff line change
@@ -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 <HOST|IP> [--port <PORT>]

.. program:: assetForgetHostKey


.. option:: --host HOST|IP

Asset whose host key should be removed

.. option:: --port PORT

Asset port serving SSH (default: 22)

1 change: 1 addition & 0 deletions doc/sphinx/plugins/restricted/index.rst
Original file line number Diff line number Diff line change
@@ -25,6 +25,7 @@ restricted plugins
accountUnexpire
accountUnfreeze
accountUnlock
assetForgetHostKey
groupCreate
groupDelete
realmCreate
2 changes: 2 additions & 0 deletions etc/sudoers.d/osh-plugin-assetForgetHostKey
Original file line number Diff line number Diff line change
@@ -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 *
14 changes: 12 additions & 2 deletions lib/perl/OVH/Bastion/allowkeeper.inc
Original file line number Diff line number Diff line change
@@ -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);
29 changes: 13 additions & 16 deletions tests/functional/tests.d/330-selfkeys.sh
Original file line number Diff line number Diff line change
@@ -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
73 changes: 73 additions & 0 deletions tests/functional/tests.d/345-assetforgethostkey.sh
Original file line number Diff line number Diff line change
@@ -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 [email protected]
contain "Connecting..."

run a2_connect_localhost1_226 $a2 [email protected] -p 226
contain "Connecting..."

run a2_connect_localhost1 $a2 [email protected]
contain "Connecting..."

run a2_connect_localhost2 $a2 [email protected]
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