Skip to content

Commit

Permalink
feat: IPv6 support
Browse files Browse the repository at this point in the history
  • Loading branch information
speed47 committed Dec 18, 2024
1 parent a97db44 commit 6198eff
Show file tree
Hide file tree
Showing 10 changed files with 184 additions and 44 deletions.
13 changes: 7 additions & 6 deletions bin/shell/osh.pl
Original file line number Diff line number Diff line change
Expand Up @@ -600,8 +600,7 @@ sub main_exit {
if ($user && !OVH::Bastion::is_valid_remote_user(user => $user, allowWildcards => ($osh_command ? 1 : 0))) {
main_exit OVH::Bastion::EXIT_INVALID_REMOTE_USER, 'invalid_remote_user', "Remote user name '$user' seems invalid";
}
if ($host && $host !~ m{^[a-zA-Z0-9._/:-]+$}) {

if ($host && $host !~ m{^\[?[a-zA-Z0-9._/:-]+\]?$}) {
# can be an IP (v4 or v6), hostname, or prefix (with a /)
main_exit OVH::Bastion::EXIT_INVALID_REMOTE_HOST, 'invalid_remote_host', "Remote host name '$host' seems invalid";
}
Expand All @@ -612,7 +611,6 @@ sub main_exit {

# if: avoid loading Net::IP and BigInt if there's no host specified
if ($host) {

# probably this "host" is in fact an option, but we didn't parse it because it's an unknown one,
# so we call the long_help() for the user, before exiting
if ($host =~ m{^--}) {
Expand All @@ -624,14 +622,17 @@ sub main_exit {
$fnret = OVH::Bastion::get_ip(host => $host);
}
if (!$fnret) {

# exit error when not osh ...
# exit error when not a plugin call
if (!$osh_command) {
main_exit OVH::Bastion::EXIT_HOST_NOT_FOUND, 'host_not_found', "Unable to resolve host '$host' ($fnret)";
}
elsif ($host && $host !~ m{^[0-9.:]+/\d+$}) # in some osh plugins, ip/mask is accepted, don't yell.
{
osh_warn("I was unable to resolve host '$host'. Something shitty might happen.");
osh_warn(
"Unable to resolve host '$host'. Trying to proceed with $osh_command anyway, but things might go wrong.");
if (index($host, ':') >= 0 && !OVH::Bastion::config('IPv6Allowed')->value) {
osh_warn("Note that '$host' looks like an IPv6 but IPv6 support has not been enabled.");
}
}
}
else {
Expand Down
78 changes: 62 additions & 16 deletions etc/bastion/bastion.conf.dist
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@
#@ at a later time.
###############################################################################
{



################
# > Main Options
################
# >> Those are the options you should customize when first setting up a bastion. All the other options have sane defaults and can be customized later if needed.
#


# bastionName (string)
# DESC: This will be the name advertised in the aliases admins will give to bastion users, and also in the banner of the plugins output. You can see it as a friendly name everybody will use to refer to this machine: something more friendly than just its full hostname.
# DEFAULT: "fix-my-config-please-missing-bastion-name"
Expand Down Expand Up @@ -49,11 +54,15 @@
# DESC: The list of accounts that are "Super Owners". They can run all group administrative commands, exactly as if they were implicitly owners of all the groups. Super Owners are only here as a last resort when the owners/gatekeepers/aclkeepers of a group are not available. Every command run by a Super Owner that would have failed if the account was not a Super Owner is logged explicitly as "Super Owner Override", you might want to add a rule for those in your SIEM. You can consider than the Super Owners have an implicit *sudo* for group management. Don't add here accounts that are bastion Admins, as they already inherit the Super Owner role. Don't forget to add them to the ``osh-superowner`` group too (system-wise), or they won't really be considered as "Super Owners": this is an additional security measure against privilege escalation.
# DEFAULT: []
"superOwnerAccounts": [],
#



################
# > SSH Policies
################
# >> All the options related to the SSH configuration and policies, both for ingress and egress connections.
#


# allowedIngressSshAlgorithms (array of strings (algorithm names))
# DESC: The algorithms authorized for ingress ssh public keys added to this bastion. Possible values: ``rsa``, ``ecdsa``, ``ed25519``, ``ecdsa-sk``, ``ed25519-sk``, note that some of those might not be supported by your current version of ``OpenSSH``: unsupported algorithms are automatically omitted at runtime.
# DEFAULT: [ "rsa", "ecdsa", "ed25519" ]
Expand Down Expand Up @@ -114,11 +123,15 @@
# DEFAULT: ""
# EXAMPLE: "-s -p 40000:49999"
"moshCommandLine": "",
#



###########################
# > Global network policies
###########################
# >> Those options can set a few global network policies to be applied bastion-wide.
#


# dnsSupportLevel (integer between 0 and 2)
# DESC: If set to 0, The Bastion will never attempt to do DNS or reverse-DNS resolutions, and return an error if you request connection to a hostname instead of an IP. Use this if you know there's no working DNS in your environment and only use IPs everywhere.
# If set to 1, The Bastion will not attempt to do DNS or reverse-DNS resolutions unless you force it to (i.e. by requesting connection to a hostname instead of an IP). You may use this if for example you have well-known hostnames in /etc/hosts, but don't have a working DNS (which would imply that reverse-DNS resolutions will always fail).
Expand Down Expand Up @@ -173,10 +186,23 @@
# DEFAULT: []
"ingressToEgressRules": [],
#
# IPv4Allowed (boolean)
# DESC: If enabled, IPv4 egress connections will be allowed, and IPv4 will be enabled in the DNS queries. This is the default. Do NOT disable this unless you enable IPv6Allowed, if you need to have an IPv6-only bastion.
# DEFAULT: true
"IPv4Allowed": true,
#
# IPv6Allowed (boolean)
# DESC: If enabled, IPv6 egress connections will be allowed, and IPv6 will be enabled in the DNS queries. By default, only IPv4 is allowed.
# DEFAULT: false
"IPv6Allowed": false,


###########
# > Logging
###########
# >> Options to customize how logs should be produced.
#


# enableSyslog (boolean)
# DESC: If enabled, we'll send logs through syslog, don't forget to setup your syslog daemon!. You can also adjust ``syslogFacility`` and ``syslogDescription`` below, to match your syslog configuration. Note that the provided ``syslog-ng`` templates work with the default values left as-is.
# DEFAULT: true
Expand Down Expand Up @@ -229,11 +255,15 @@
# EXAMPLE: "^rsync --server .+"
# DEFAULT: ""
"ttyrecStealthStdoutPattern": "",
#



##########################
# > Other ingress policies
##########################
# >> Policies applying to the ingress connections
#


# ingressKeysFrom (array of strings (list of IPs and/or prefixes))
# DESC: This array of IPs (or prefixes, such as ``10.20.30.0/24``) will be used to build the ``from="..."`` in front of the ingress account public keys used to connect to the bastion (in ``accountCreate`` or ``selfAddIngressKey``). If the array is empty, then **NO** ``from="..."`` is added (this lowers the security).
# DEFAULT: []
Expand All @@ -245,11 +275,15 @@
# Note that when no user-specified ``from="..."`` appears, the value of ``ingressKeysFrom`` is still used, regardless of this option.
# DEFAULT: false
"ingressKeysFromAllowOverride": false,
#



#########################
# > Other egress policies
#########################
# >> Policies applying to the egress connections
#


# defaultLogin (string)
# DESC: The default remote user to use for egress ssh connections where no user has been specified by our caller. If set to the empty string (``""``), will default to the account name of the caller. If your bastion is mainly used to connect as ``root`` on remote systems, you might want to set this to ``root`` for example, to spare a few keystrokes to your users. This is only used when no user is specified on the connection line. For example if your bastion alias is ``bssh``, and you say ``bssh srv1.example.net``, the value of the ``defaultLogin`` value will be used as the user to login as remotely.
# DEFAULT: ""
Expand All @@ -274,11 +308,15 @@
# DESC: If set to ``true``, will allow telnet egress connections (``-e`` / ``--telnet``).
# DEFAULT: false
"telnetAllowed": false,
#



####################
# > Session policies
####################
# >> Options to customize the established sessions behaviour
#


# displayLastLogin (boolean)
# DESC: If ``true``, display their last login information on connection to your users.
# DEFAULT: true
Expand Down Expand Up @@ -361,11 +399,15 @@
# DESC: List of accounts which should NOT be checked against the ``accountExternalValidationProgram`` mechanism above (for example bot accounts). This can also be set per-account at account creation time or later with the ``accountModify`` plugin's ``--always-active`` flag.
# DEFAULT: []
"alwaysActiveAccounts": [],
#



####################
# > Account policies
####################
# >> Policies applying to the bastion accounts themselves
#


# accountMaxInactiveDays (int >= 0 (days))
# DESC: If > 0, deny access to accounts that didn't log in since at least that many days. A value of 0 means that this functionality is disabled (we will never deny access for inactivity reasons).
# DEFAULT: 0
Expand Down Expand Up @@ -439,11 +481,15 @@
# - duo: enable the use of the Duo PAM module (pam_duo.so), of course you need to set it up correctly in your `/etc/pam.d/sshd` file.
# DEFAULT: 'google-authenticator'
"TOTPProvider": "google-authenticator",
#



#################
# > Other options
#################
# >> These options are either discouraged (in which case this is explained in the description) or rarely need to be modified.
#


# accountUidMin (int >= 100)
# DESC: Minimum allowed UID for accounts on this bastion. Hardcoded > 100 even if configured for less.
# DEFAULT: 2000
Expand Down
35 changes: 26 additions & 9 deletions lib/perl/OVH/Bastion.pm
Original file line number Diff line number Diff line change
Expand Up @@ -700,15 +700,26 @@ sub is_valid_ip {
return R('KO_INVALID_IP', msg => "Invalid IP address ($ip)");
}

my $shortip = $IpObject->prefix;

# if /32 or /128, omit the /prefixlen on $shortip
my $type = 'prefix';
if ( ($IpObject->version == 4 and $IpObject->prefixlen == 32)
or ($IpObject->version == 6 and $IpObject->prefixlen == 128))
{
$shortip =~ s'/\d+$'';
$type = 'single';
my ($shortip, $type);
if ($IpObject->version == 4) {
if ($IpObject->prefixlen == 32) {
$shortip = $IpObject->ip;
$type = 'single';
}
else {
$shortip = $IpObject->prefix;
$type = 'prefix';
}
}
elsif ($IpObject->version == 6) {
if ($IpObject->prefixlen == 128) {
$shortip = $IpObject->short;
$type = 'single';
}
else {
$shortip = $IpObject->short . '/' . $IpObject->prefixlen;
$type = 'prefix';
}
}

if (not $allowPrefixes and $type eq 'prefix') {
Expand Down Expand Up @@ -1125,6 +1136,12 @@ sub build_ttyrec_cmdline_part1of2 {
return R('ERR_MISSING_PARAMETER', msg => "Missing ip parameter");
}

# if ip is an IPv6, replace :'s by .'s and surround by v6[]'s (which is allowed on all filesystems)
if ($params{'ip'} && index($params{'ip'}, ':') >= 0) {
$params{'ip'} =~ tr/:/./;
$params{'ip'} = 'v6[' . $params{'ip'} . ']';
}

# build ttyrec filename format
my $bastionName = OVH::Bastion::config('bastionName')->value;
my $ttyrecFilenameFormat = OVH::Bastion::config('ttyrecFilenameFormat')->value;
Expand Down
10 changes: 5 additions & 5 deletions lib/perl/OVH/Bastion/allowdeny.inc
Original file line number Diff line number Diff line change
Expand Up @@ -280,16 +280,16 @@ sub is_access_way_granted {
sub get_ip {
my %params = @_;
my $host = $params{'host'};
my $v4 = $params{'v4'}; # allow ipv4 ?
my $v6 = $params{'v6'}; # allow ipv6 ?
my $v4 = $params{'v4'} // OVH::Bastion::config('IPv4Allowed')->value;
my $v6 = $params{'v6'} // OVH::Bastion::config('IPv6Allowed')->value;

if (!$host) {
return R('ERR_MISSING_PARAMETER', msg => "Missing parameter 'host'");
}

# by default, only v4 unless specified otherwise
$v4 = 1 if not defined $v4;
$v6 = 0 if not defined $v6;
# if v4 or v6 are disabled in config, force-disable them here too
$v4 = 0 if !OVH::Bastion::config('IPv4Allowed')->value;
$v6 = 0 if !OVH::Bastion::config('IPv6Allowed')->value;

# try to see if it's already an IP
osh_debug("checking if '$host' is already an IP");
Expand Down
2 changes: 0 additions & 2 deletions lib/perl/OVH/Bastion/allowkeeper.inc
Original file line number Diff line number Diff line change
Expand Up @@ -592,11 +592,9 @@ sub access_modify {

# if we're adding it, append other parameters as comments
if ($action eq 'add') {

$entry .= " $entryComment";

if ($forceKey) {

# hash is case-sensitive only for new SHA256 format
$forceKey = lc($forceKey) if ($forceKey !~ /^sha256:/i);
$entry .= " # FORCEKEY=" . $forceKey;
Expand Down
4 changes: 2 additions & 2 deletions lib/perl/OVH/Bastion/configuration.inc
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ sub load_configuration {
options => [
qw{
enableSyslog enableGlobalAccessLog enableAccountAccessLog enableGlobalSqlLog enableAccountSqlLog displayLastLogin
interactiveModeByDefault interactiveModeProactiveMFAenabled
interactiveModeByDefault interactiveModeProactiveMFAenabled IPv4Allowed
}
],
},
Expand All @@ -275,7 +275,7 @@ sub load_configuration {
qw{
interactiveModeAllowed readOnlySlaveMode sshClientHasOptionE ingressKeysFromAllowOverride
moshAllowed debug keyboardInteractiveAllowed passwordAllowed telnetAllowed remoteCommandEscapeByDefault
accountExternalValidationDenyOnFailure ingressRequirePIV
accountExternalValidationDenyOnFailure ingressRequirePIV IPv6Allowed
}
],
}
Expand Down
8 changes: 7 additions & 1 deletion tests/functional/docker/docker_build_and_run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,13 @@ docker rm -f "bastion_${target}_tester" 2>/dev/null || true
if docker inspect "bastion-$target" >/dev/null 2>&1; then
docker network rm "bastion-$target" >/dev/null
fi
docker network create "bastion-$target" >/dev/null

# trying with IPv6
if ! docker network create --ipv6 --subnet fd42:cafe:efac:"$(printf "%x" $RANDOM)"::/64 "bastion-$target" >/dev/null; then
# didn't work... retry without IPv6
echo "... IPv6 is not enabled in docker daemon, falling back to IPv4-only network"
docker network create "bastion-$target" >/dev/null
fi

# run target but force entrypoint to test one, and add some keys in env (will be shared with tester)
echo "Starting target instance"
Expand Down
2 changes: 1 addition & 1 deletion tests/functional/docker/target_role.sh
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ if [ "$OS_FAMILY" = Linux ] ; then

elif [ "$OS_FAMILY" = OpenBSD ] || [ "$OS_FAMILY" = FreeBSD ] || [ "$OS_FAMILY" = NetBSD ] ; then
# setup some 127.0.0.x IPs (needed for our tests)
# this automatically works under Linux on lo
# this is not required under Linux where all IPs of 127.0.0.0/8 implicitely work
nic=$(ifconfig | perl -ne 'm{^([a-z._0-9]+): flags}i and $nic=$1; m{inet 127\.0\.0\.1} and print $nic and exit')
: "${nic:=lo0}"
i=2
Expand Down
Loading

0 comments on commit 6198eff

Please sign in to comment.