diff --git a/bin/shell/osh.pl b/bin/shell/osh.pl index c35a1c27c..a5e3db025 100755 --- a/bin/shell/osh.pl +++ b/bin/shell/osh.pl @@ -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"; } @@ -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{^--}) { @@ -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 { diff --git a/etc/bastion/bastion.conf.dist b/etc/bastion/bastion.conf.dist index 7376604ba..e2d47b863 100644 --- a/etc/bastion/bastion.conf.dist +++ b/etc/bastion/bastion.conf.dist @@ -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" @@ -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" ] @@ -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). @@ -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 @@ -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: [] @@ -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: "" @@ -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 @@ -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 @@ -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 diff --git a/lib/perl/OVH/Bastion.pm b/lib/perl/OVH/Bastion.pm index c02f57e03..150080a8c 100644 --- a/lib/perl/OVH/Bastion.pm +++ b/lib/perl/OVH/Bastion.pm @@ -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') { @@ -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; diff --git a/lib/perl/OVH/Bastion/allowdeny.inc b/lib/perl/OVH/Bastion/allowdeny.inc index 70ab3cd22..1dbdcdb06 100644 --- a/lib/perl/OVH/Bastion/allowdeny.inc +++ b/lib/perl/OVH/Bastion/allowdeny.inc @@ -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"); diff --git a/lib/perl/OVH/Bastion/allowkeeper.inc b/lib/perl/OVH/Bastion/allowkeeper.inc index 8a3281f19..1c24bb072 100644 --- a/lib/perl/OVH/Bastion/allowkeeper.inc +++ b/lib/perl/OVH/Bastion/allowkeeper.inc @@ -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; diff --git a/lib/perl/OVH/Bastion/configuration.inc b/lib/perl/OVH/Bastion/configuration.inc index 137e75bdb..0009f9739 100644 --- a/lib/perl/OVH/Bastion/configuration.inc +++ b/lib/perl/OVH/Bastion/configuration.inc @@ -265,7 +265,7 @@ sub load_configuration { options => [ qw{ enableSyslog enableGlobalAccessLog enableAccountAccessLog enableGlobalSqlLog enableAccountSqlLog displayLastLogin - interactiveModeByDefault interactiveModeProactiveMFAenabled + interactiveModeByDefault interactiveModeProactiveMFAenabled IPv4Allowed } ], }, @@ -275,7 +275,7 @@ sub load_configuration { qw{ interactiveModeAllowed readOnlySlaveMode sshClientHasOptionE ingressKeysFromAllowOverride moshAllowed debug keyboardInteractiveAllowed passwordAllowed telnetAllowed remoteCommandEscapeByDefault - accountExternalValidationDenyOnFailure ingressRequirePIV + accountExternalValidationDenyOnFailure ingressRequirePIV IPv6Allowed } ], } diff --git a/tests/functional/docker/docker_build_and_run_tests.sh b/tests/functional/docker/docker_build_and_run_tests.sh index 8602d7a8e..815ed83d9 100755 --- a/tests/functional/docker/docker_build_and_run_tests.sh +++ b/tests/functional/docker/docker_build_and_run_tests.sh @@ -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" diff --git a/tests/functional/docker/target_role.sh b/tests/functional/docker/target_role.sh index bf4be3438..c1daf9f1b 100755 --- a/tests/functional/docker/target_role.sh +++ b/tests/functional/docker/target_role.sh @@ -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 diff --git a/tests/functional/tests.d/355-ipv6.sh b/tests/functional/tests.d/355-ipv6.sh new file mode 100644 index 000000000..ad5fbc687 --- /dev/null +++ b/tests/functional/tests.d/355-ipv6.sh @@ -0,0 +1,72 @@ +# 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_ipv6() +{ + # create account1 + success accountCreate $a0 --osh accountCreate --always-active --account $account1 --uid $uid1 --public-key "\"$(cat $account1key1file.pub)\"" + json .error_code OK .command accountCreate .value null + + plgfail use_ipv6_notenabled_1 $a0 --osh selfAddPersonalAccess --host ::1 --force --user-any --port-any + contain 'Unable to resolve host' + contain 'looks like an IPv6' + json .command selfAddPersonalAccess .error_code ERR_MISSING_PARAMETER + + plgfail use_ipv6_notenabled_2 $a0 --osh selfAddPersonalAccess --host '[::1]' --force --user-any --port-any + contain 'Unable to resolve host' + contain 'looks like an IPv6' + json .command selfAddPersonalAccess .error_code ERR_MISSING_PARAMETER + + # now enable IPv6 + configchg 's=^\\\\x22IPv6Allowed\\\\x22.+=\\\\x22IPv6Allowed\\\\x22:true,=' + + success add_access_ipv6 $a0 --osh selfAddPersonalAccess --host '::1' --force --user-any --port-any + nocontain "already" + contain "Forcing add as asked" + json .command selfAddPersonalAccess .error_code OK .value.ip ::1 .value.port null .value.user null + + success add_access_ipv6_dupe $a0 --osh selfAddPersonalAccess --host '::1' --force --user-any --port-any + contain "already" + json .command selfAddPersonalAccess .error_code OK_NO_CHANGE + + success add_access_ipv6_multiformat $a0 --osh selfAddPersonalAccess --host 'fe80:cafe::000f:ff' --force --user-any --port-any + nocontain "already" + json .command selfAddPersonalAccess .error_code OK .value.ip fe80:cafe::f:ff .value.port null .value.user null + + success add_access_ipv6_multiformat_dupe1 $a0 --osh selfAddPersonalAccess --host 'fe80:cafe:0000:0000:0000:0000:000f:00ff' --force --user-any --port-any + contain "already" + json .command selfAddPersonalAccess .error_code OK_NO_CHANGE + + success add_access_ipv6_multiformat_dupe2 $a0 --osh selfAddPersonalAccess --host 'fe80:cafe:00::0:f:ff' --force --user-any --port-any + contain "already" + json .command selfAddPersonalAccess .error_code OK_NO_CHANGE + + success self_listaccesses $a0 --osh selfListAccesses + json .command selfListAccesses .error_code OK + json --splitsort '[.value[]|select(.type == "personal").acl[]|.ip]' '::1 fe80:cafe::f:ff' + + run connect_ipv6 $a0 ::1 + contain "Connecting..." + contain "$account0.v6[..1].22.ttyrec" + + success self_delaccess $a0 --osh selfDelPersonalAccess --host 'fe80:cafe:0:00:0::f:ff' --port '*' --user '*' + json .command selfDelPersonalAccess .error_code OK + + success self_delaccess_dupe $a0 --osh selfDelPersonalAccess --host 'fe80:cafe:00:0:00::f:ff' --port '*' --user '*' + json .command selfDelPersonalAccess .error_code OK_NO_CHANGE + + success self_listaccesses_2 $a0 --osh selfListAccesses + json .command selfListAccesses .error_code OK + json --splitsort '[.value[]|select(.type == "personal").acl[]|.ip]' '::1' + + # delete account1 + script cleanup $a0 --osh accountDelete --account $account1 "<<< \"Yes, do as I say and delete $account1, kthxbye\"" + retvalshouldbe 0 +} + +testsuite_ipv6 +unset -f testsuite_ipv6 diff --git a/tests/functional/tests.d/395-mfa-scp-sftp-rsync.sh b/tests/functional/tests.d/395-mfa-scp-sftp-rsync.sh index 1ee07625a..2cb73402b 100644 --- a/tests/functional/tests.d/395-mfa-scp-sftp-rsync.sh +++ b/tests/functional/tests.d/395-mfa-scp-sftp-rsync.sh @@ -152,11 +152,11 @@ EOF run invalidhostname_scp_oldhelper scp $scp_options -F $mytmpdir/ssh_config -S /tmp/scphelper -i $account0key1file $shellaccount@_invalid._invalid:uptest /tmp/downloaded retvalshouldbe 1 - contain REGEX "Sorry, couldn't resolve the host you specified|I was unable to resolve host" + contain REGEX "Sorry, couldn't resolve the host you specified|I was unable to resolve host|Unable to resolve host" run invalidhostname_scp_newwrapper /tmp/scpwrapper -i $account0key1file $shellaccount@_invalid._invalid:uptest /tmp/downloaded retvalshouldbe 1 - contain REGEX "Sorry, couldn't resolve the host you specified|I was unable to resolve host" + contain REGEX "Sorry, couldn't resolve the host you specified|I was unable to resolve host|Unable to resolve host" success personal_scp_upload_oldhelper_ok scp $scp_options -F $mytmpdir/ssh_config -S /tmp/scphelper -i $account0key1file /etc/passwd $shellaccount@127.0.0.2:uptest contain "through the bastion to"