feat(portforwarding): allow portforwarding on master instances only

pull/597/head
jon4hz 5 months ago
parent fa73b11e45
commit 9cb87d3d89
No known key found for this signature in database
GPG Key ID: 4B0AFE9E7118898E

@ -35,8 +35,6 @@ timeout=120
rshcmd=""
remoteuser="bastionsync"
remotehostlist=""
reload_sshd_command="/usr/bin/systemctl reload sshd"
# old deprecated config param:
remotehost=""
@ -98,7 +96,6 @@ do
echo /home/allowkeeper
echo /home/keykeeper
echo /home/passkeeper
test -d /etc/ssh/sshd_config.forward.d && echo /etc/ssh/sshd_config.forward.d
# all allowed.ip files of bastion groups:
for grouphome in $(getent group | grep -Eo '^key[a-zA-Z0-9_-]+' | grep -Ev -- '-(aclkeeper|gatekeeper|owner)$' | sed 's=^=/home/='); do
test -e "$grouphome/allowed.ip" && echo "$grouphome/allowed.ip"
@ -142,50 +139,22 @@ do
remoteport=22
fi
_log "$remote: [Server $((i+1))/$remotehostslen - Step 1/4] syncing needed data..."
rsync_output=$(rsync -vaA --numeric-ids --delete --filter "merge $rsyncfilterfile" --rsh "$rshcmd -p $remoteport" / "$remoteuser@$remote:/" 2>&1); ret=$?
echo "$rsync_output"
_log "$remote: [Server $((i+1))/$remotehostslen - Step 1/4] sync ended with return value $ret"
_log "$remote: [Server $((i+1))/$remotehostslen - Step 1/3] syncing needed data..."
rsync -vaA --numeric-ids --delete --filter "merge $rsyncfilterfile" --rsh "$rshcmd -p $remoteport" / "$remoteuser@$remote:/"; ret=$?
_log "$remote: [Server $((i+1))/$remotehostslen - Step 1/3] sync ended with return value $ret"
if [ "$ret" != 0 ]; then (( ++nberrs )); fi
if echo "$rsync_output" | grep -q "etc/ssh/sshd_config\.forward\.d/"; then
ssh_config_changed=1
fi
_log "$remote: [Server $((i+1))/$remotehostslen - Step 2/4] syncing lastlog files from master to slave, only if master version is newer..."
_log "$remote: [Server $((i+1))/$remotehostslen - Step 2/3] syncing lastlog files from master to slave, only if master version is newer..."
rsync -vaA --numeric-ids --update --include '/' --include '/home/' --include '/home/*/' --include '/home/*/lastlog' \
--exclude='*' --rsh "$rshcmd -p $remoteport" / "$remoteuser@$remote:/"; ret=$?
_log "$remote: [Server $((i+1))/$remotehostslen - Step 2/4] sync ended with return value $ret"
_log "$remote: [Server $((i+1))/$remotehostslen - Step 2/3] sync ended with return value $ret"
if [ "$ret" != 0 ]; then (( ++nberrs )); fi
_log "$remote: [Server $((i+1))/$remotehostslen - Step 3/4] syncing lastlog files from slave to master, only if slave version is newer..."
_log "$remote: [Server $((i+1))/$remotehostslen - Step 3/3] syncing lastlog files from slave to master, only if slave version is newer..."
find /home -mindepth 2 -maxdepth 2 -type f -name lastlog | rsync -vaA --numeric-ids --update --prune-empty-dirs --include='/' \
--include='/home' --include='/home/*/' --include-from=- --exclude='*' --rsh "$rshcmd -p $remoteport" "$remoteuser@$remote:/" /; ret=$?
_log "$remote: [Server $((i+1))/$remotehostslen - Step 3/4] sync ended with return value $ret"
_log "$remote: [Server $((i+1))/$remotehostslen - Step 3/3] sync ended with return value $ret"
if [ "$ret" != 0 ]; then (( ++nberrs )); fi
_log "$remote: [Server $((i+1))/$remotehostslen - Step 4/4] checking if sshd config reload is needed..."
if [ "$ssh_config_changed" = "1" ] && [ -d /etc/ssh/sshd_config.forward.d ]; then
$rshcmd -p "$remoteport" "$remoteuser@$remote" "test -d /etc/ssh/sshd_config.forward.d"; ret=$?
if [ "$ret" = 0 ]; then
cmd_output=$($rshcmd -p "$remoteport" "$remoteuser@$remote" "sudo $reload_sshd_command" 2>&1); ret=$?
echo "$cmd_output"
if [ "$ret" != 0 ]; then
(( ++nberrs ))
_warn "$remote: [Server $((i+1))/$remotehostslen - Step 4/4] failed to reload sshd configuration"
else
_log "$remote: [Server $((i+1))/$remotehostslen - Step 4/4] sshd configuration reloaded successfully"
fi
else
_log "$remote: [Server $((i+1))/$remotehostslen - Step 4/4] ssh config directory not found on slave, skipping reload"
fi
elif [ "$ssh_config_changed" != "1" ]; then
_log "$remote: [Server $((i+1))/$remotehostslen - Step 4/4] no ssh port forwarding config changes detected, skipping sshd reload"
else
_log "$remote: [Server $((i+1))/$remotehostslen - Step 4/4] no ssh port forwarding configs found, skipping sshd reload"
fi
done
if [ "$nberrs" = 0 ]; then
@ -193,4 +162,4 @@ do
else
_err "Encountered $nberrs error(s) while synchronizing, see above"
fi
done
done

@ -0,0 +1,146 @@
#! /usr/bin/perl -T
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
# SUDOERS %osh-admin ALL=(root) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-adminGenerateAllSshdConfigs
# SUDOERS %osh-admin ALL=(root) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-adminGenerateAllSshdConfigs --slave-mode
# FILEMODE 0700
# FILEOWN 0 0
#>HEADER
use common::sense;
use Getopt::Long qw(:config no_auto_abbrev no_ignore_case);
use File::Basename;
use lib dirname(__FILE__) . '/../../lib/perl';
use OVH::Bastion;
use OVH::Bastion::Helper;
# Fetch command options
my $fnret;
my ($result, @optwarns);
eval {
local $SIG{__WARN__} = sub { push @optwarns, shift };
$result = GetOptions();
};
if ($@) { die $@ }
OVH::Bastion::Helper::check_spurious_args();
if (!$result) {
local $" = ", ";
HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns");
}
#<HEADER
#>RIGHTSCHECK
# Only admins can run this
if ($self eq 'root') {
osh_debug "Real root, skipping checks of permissions";
}
else {
# need to perform another security check
$fnret = OVH::Bastion::is_user_in_group(user => $self, group => "osh-admin");
if (!$fnret) {
HEXIT('ERR_SECURITY_VIOLATION', msg => "You're not allowed to run this, dear $self");
}
}
#<RIGHTSCHECK
#>CODE
$fnret = OVH::Bastion::load_configuration();
$fnret or main_exit(OVH::Bastion::EXIT_CONFIGURATION_FAILURE, "configuration_failure", $fnret->msg);
my $config = $fnret->value;
my $slaveMode = $config->{'readOnlySlaveMode'};
if ($slaveMode) {
osh_warn "This bastion is in slave mode (readOnlySlaveMode).";
osh_warn "All port forwarding configurations will be REMOVED for all accounts.";
}
else {
osh_info "This bastion is in master mode.";
osh_info "Port forwarding configurations will be regenerated for all accounts.";
}
# Get all accounts
$fnret = OVH::Bastion::get_account_list();
$fnret or HEXIT($fnret);
my @accounts = sort keys %{$fnret->value};
my $totalAccounts = scalar(@accounts);
osh_info("Found $totalAccounts accounts to process");
# Acquire lock ONCE for all accounts to ensure thread-safe config generation
$fnret = OVH::Bastion::Helper::get_lock_fh(category => 'portforwarding');
$fnret or HEXIT($fnret);
my $lock_fh = $fnret->value;
$fnret = OVH::Bastion::Helper::acquire_lock($lock_fh);
$fnret or HEXIT($fnret);
my @success;
my @errors;
my @skipped;
foreach my $account (@accounts) {
osh_info("Processing account: $account");
# Validate account exists
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account);
if (!$fnret) {
push @skipped, "$account: " . $fnret->msg;
osh_warn(" Skipped: " . $fnret->msg);
next;
}
$account = $fnret->value->{'account'}; # untainted
if ($slaveMode) {
# In slave mode, we remove all port forwards by removing the config file
my $config_dir = "/etc/ssh/sshd_config.forward.d";
my $config_file = "$config_dir/$account.conf";
# Remove the config file if it exists
if (-e $config_file) {
if (!unlink($config_file)) {
push @errors, "$account: Failed to remove config file: $!";
osh_warn(" Failed to remove config file: $!");
next;
}
osh_info(" Removed port forwarding config file (slave mode)");
}
else {
osh_info(" No port forwarding config file to remove (slave mode)");
}
push @success, $account;
}
else {
# In master mode, regenerate normally
$fnret = OVH::Bastion::generate_account_sshd_forwarding_config(account => $account);
if ($fnret) {
push @success, $account;
osh_info(" Success");
}
else {
push @errors, "$account: " . $fnret->msg;
osh_warn(" Failed: " . $fnret->msg);
}
}
}
HEXIT(
'OK',
msg => "Processed "
. scalar(@success)
. " accounts successfully"
. (@errors ? ", " . scalar(@errors) . " failed" : "")
. (@skipped ? ", " . scalar(@skipped) . " skipped" : ""),
value => {
successful => \@success,
failed => \@errors,
skipped => \@skipped,
total => $totalAccounts,
slave_mode => $slaveMode,
}
);

@ -0,0 +1,73 @@
#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;
use Term::ANSIColor;
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 => "generate sshd configs for all accounts",
options => {},
helptext => <<'EOF',
Generate sshd port forwarding configs for all accounts
Usage: --osh SCRIPT_NAME
This command will regenerate the sshd port forwarding configuration for all accounts.
If this bastion is a slave (readOnlySlaveMode), all port forwarding entries will be removed.
EOF
);
# Call helper to process all accounts (acquires lock once)
my @command = qw{ sudo -n -u root -- /usr/bin/env perl -T };
push @command, $OVH::Bastion::BASEPATH . '/bin/helper/osh-adminGenerateAllSshdConfigs';
my $fnret = OVH::Bastion::helper(cmd => \@command);
$fnret or osh_exit $fnret;
# Extract results from helper
my $result = $fnret->value;
my @success = @{$result->{'successful'} || []};
my @errors = @{$result->{'failed'} || []};
my @skipped = @{$result->{'skipped'} || []};
my $totalAccounts = $result->{'total'} || 0;
# Print summary
osh_info "";
osh_info "=" x 60;
osh_info "Summary:";
osh_info " Total accounts: $totalAccounts";
osh_info " Successful: " . colored(scalar(@success), 'green');
osh_info " Failed: " . colored(scalar(@errors), @errors ? 'red' : 'green');
osh_info " Skipped: " . colored(scalar(@skipped), @skipped ? 'yellow' : 'green');
if (@errors) {
osh_info "";
osh_info "Failed accounts:";
foreach my $error (@errors) {
osh_info " - $error";
}
}
if (@skipped) {
osh_info "";
osh_info "Skipped accounts:";
foreach my $skip (@skipped) {
osh_info " - $skip";
}
}
if (@errors) {
osh_exit R(
'ERR_PARTIAL_FAILURE',
msg => "Processed " . scalar(@success) . " accounts successfully, but " . scalar(@errors) . " failed",
value => $result
);
}
osh_ok($result);

@ -15,38 +15,14 @@ fi
shift
# shellcheck disable=SC2068
set -- $@
if [ "$1 $2" = "rsync --server" ]; then
shift
shift
if ! cd /; then
echo "Failed to chdir /, aborting" >&2
exit 6
fi
exec /usr/bin/sudo -- /usr/bin/rsync --server "$@"
# TODO: make this less cursed
elif [ "$1" = "test" ] && [ "$2" = "-d" ] && [ "$3" = "/etc/ssh/sshd_config.forward.d" ]; then
exec test -d /etc/ssh/sshd_config.forward.d
elif [ "$*" = "test -d /etc/ssh/sshd_config.forward.d" ]; then
exec test -d /etc/ssh/sshd_config.forward.d
elif [ "$*" = "sudo /bin/systemctl reload sshd" ]; then
exec /usr/bin/sudo /bin/systemctl reload sshd
elif [ "$*" = "sudo /usr/bin/systemctl reload sshd" ]; then
exec /usr/bin/sudo /usr/bin/systemctl reload sshd
elif [ "$*" = "sudo /bin/systemctl reload ssh" ]; then
exec /usr/bin/sudo /bin/systemctl reload ssh
elif [ "$*" = "sudo /usr/bin/systemctl reload ssh" ]; then
exec /usr/bin/sudo /usr/bin/systemctl reload ssh
elif [ "$*" = "sudo /usr/sbin/service ssh reload" ]; then
exec /usr/bin/sudo /usr/sbin/service ssh reload
elif [ "$*" = "sudo /usr/sbin/service sshd reload" ]; then
exec /usr/bin/sudo /usr/sbin/service sshd reload
elif [ "$*" = "sudo /etc/init.d/ssh reload" ]; then
exec /usr/bin/sudo /etc/init.d/ssh reload
elif [ "$*" = "sudo /etc/init.d/sshd reload" ]; then
exec /usr/bin/sudo /etc/init.d/sshd reload
else
echo "Only rsync and sshd reload commands are allowed, aborting" >&2
if [ "$1 $2" != "rsync --server" ]; then
echo "Only rsync is allowed, aborting" >&2
exit 5
fi
fi
shift
shift
if ! cd /; then
echo "Failed to chdir /, aborting" >&2
exit 6
fi
exec /usr/bin/sudo -- /usr/bin/rsync --server "$@"

@ -821,6 +821,15 @@ if ($portForwards && @$portForwards && !$telnet) {
'port_forwarding_disabled', "Port forwarding feature is disabled on this bastion");
}
# check if bastion is a master instance
if ($config->{'readOnlySlaveMode'}) {
main_exit(
OVH::Bastion::EXIT_ACCESS_DENIED,
'port_forwarding_disabled_slave',
"Port forwarding feature is disabled on read-only slave bastions"
);
}
# parse all the the requested port forwards
# The expected format is local_port:remote_host:remote_port
# remote_host must be the allowed IP of the the target server or localhost

@ -22,8 +22,6 @@
+ /etc/ssh/
+ /etc/ssh/ssh_host_*_key
+ /etc/ssh/ssh_host_*_key.pub
+ /etc/ssh/sshd_config.forward.d/
+ /etc/ssh/sshd_config.forward.d/*.conf
- /etc/**
+ /home/

@ -55,12 +55,4 @@ remoteuser="bastionsync"
# DESC: The list of the secondary bastions to push our data to. If this list is empty, the daemon won't do anything.
# DEFAULT: ""
# EXAMPLE: "192.0.2.17 192.0.2.12:2244"
remotehostlist=""
#
# > Portforwarding options
# >> These options are required if the portforwarding feature is enabled on the bastion
#
# reload_sshd_command
# DESC: The command to use to reload the SSH daemon on secondary bastions, when configuration changes are detected.
# DEFAULT: "/usr/bin/systemctl reload sshd"
reload_sshd_command="/usr/bin/systemctl reload sshd"
remotehostlist=""

@ -1,9 +1 @@
bastionsync ALL=(root) NOPASSWD: /usr/bin/rsync --server *
bastionsync ALL=(root) NOPASSWD: /bin/systemctl reload sshd
bastionsync ALL=(root) NOPASSWD: /usr/bin/systemctl reload sshd
bastionsync ALL=(root) NOPASSWD: /bin/systemctl reload ssh
bastionsync ALL=(root) NOPASSWD: /usr/bin/systemctl reload ssh
bastionsync ALL=(root) NOPASSWD: /usr/sbin/service ssh reload
bastionsync ALL=(root) NOPASSWD: /usr/sbin/service sshd reload
bastionsync ALL=(root) NOPASSWD: /etc/init.d/ssh reload
bastionsync ALL=(root) NOPASSWD: /etc/init.d/sshd reload
bastionsync ALL=(root) NOPASSWD: /usr/bin/rsync --server *

@ -0,0 +1,2 @@
# regenerate all sshd configs after master/slave promote/demote
%osh-admin ALL=(root) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-adminGenerateAllSshdConfigs

@ -246,7 +246,7 @@ sub generate_account_sshd_forwarding_config {
$fnret or return $fnret;
if (not @{$fnret->value}) {
osh_ok R('OK_EMPTY', msg => "$account has no accesses, no port forwarding config to generate");
return R('OK_EMPTY', msg => "$account has no accesses, no port forwarding config to generate");
}
my @allowed_ports;
@ -538,13 +538,15 @@ sub _has_sessions_before_timestamp {
osh_debug("Processing line from 'who -u': $line");
# Parse 'who -u' output to get PID
# Format: user pts/0 2024-11-18 10:30 old 12345 (192.168.1.1)
my $pid;
if ($line !~ /\s+(\d+)\s+\(/) {
osh_debug("Skipping unparseable line from 'who -u': $line");
next;
}
my $pid = $1;
osh_debug("Checking session with PID $pid");
else {
$pid = $1;
osh_debug("Checking session with PID $pid");
}
$fnret = OVH::Bastion::execute_simple(
cmd => ['ps', '-o', 'lstart=', '-D', '%s', '-p', $pid],

Loading…
Cancel
Save