new account option: mfa-any, to allow ingress login with pubkey alone or pam alone instead of requiring both

This commit is contained in:
madx 2021-09-06 14:19:53 +02:00 committed by Stéphane Lesimple
parent a65cbd55b8
commit ea8ed97a34
17 changed files with 174 additions and 39 deletions

View file

@ -872,7 +872,7 @@ if [ "$nothing" = 0 ]; then
at_least_one_error=0
for group in bastion-users \
mfa-password-reqd mfa-password-bypass mfa-password-configd \
mfa-totp-reqd mfa-totp-bypass mfa-totp-configd bastion-nopam
mfa-totp-reqd mfa-totp-bypass mfa-totp-configd bastion-nopam mfa-any
do
if getent group "$group" >/dev/null 2>&1; then
:

View file

@ -280,6 +280,49 @@ foreach my $tuple (@modify) {
}
}
}
elsif ($key eq 'mfa-any') {
$fnret = OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::MFA_ANY_GROUP);
if ($value eq 'yes') {
{
osh_info "Setting authentication requirements to pubkey OR pam (if a password/TOTP is set) for this account...";
if ($fnret) {
osh_info "... no change was required";
$result{$jsonkey} = R('OK_NO_CHANGE');
last;
}
$fnret = OVH::Bastion::sys_addmembertogroup(user => $account, group => OVH::Bastion::MFA_ANY_GROUP, noisy_stderr => 1);
if (!$fnret) {
osh_warn "... error while setting the alternative authentication option";
$result{$jsonkey} = R('ERR_ADDING_TO_GROUP');
last;
}
osh_info "... done, this account can now authenticate with either pubkey or pam (if a password/TOTP is set)";
$result{$jsonkey} = R('OK');
}
}
elsif ($value eq 'no') {
{
osh_info "Setting authentication to pubkey AND pam (if a password/TOTP is set) for this account...";
if (!$fnret) {
osh_info "... no change was required";
$result{$jsonkey} = R('OK_NO_CHANGE');
last;
}
$fnret = OVH::Bastion::sys_delmemberfromgroup(user => $account, group => OVH::Bastion::MFA_ANY_GROUP, noisy_stderr => 1);
if (!$fnret) {
osh_warn "... error while removing the alternative authentication option";
$result{$jsonkey} = R('ERR_REMOVING_FROM_GROUP');
last;
}
osh_info "... done, this account now requires to authenticate with pubkey AND pam (if a password/TOTP is set)";
$result{$jsonkey} = R('OK');
}
}
}
elsif ($key eq 'mfa-password-required') {
_mfa_toggle($key, $value, 'Password', OVH::Bastion::MFA_PASSWORD_REQUIRED_GROUP, OVH::Bastion::MFA_PASSWORD_BYPASS_GROUP);
}

View file

@ -267,6 +267,9 @@ if (OVH::Bastion::is_auditor(account => $self)) {
$ret{'pam_auth_bypass'} = OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::PAM_AUTH_BYPASS_GROUP) ? 1 : 0;
osh_info "- PAM authentication bypass is " . ($ret{'pam_auth_bypass'} ? colored('enabled', 'green') : colored('disabled', 'blue'));
$ret{'mfa_any'} = OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::MFA_ANY_GROUP) ? 1 : 0;
osh_info "- Alternative authentication logic (allow both pubkey alone and PAM alone) is " . ($ret{'mfa_any'} ? colored('enabled', 'green') : colored('disabled', 'blue'));
$ret{'personal_egress_mfa_required'} = OVH::Bastion::account_config(account => $account, key => "personal_egress_mfa_required")->value;
$ret{'personal_egress_mfa_required'} ||= 'none'; # no config means no mfa
osh_info "- MFA policy on personal accesses (using personal keys) on egress side is: " . $ret{'personal_egress_mfa_required'};

View file

@ -138,6 +138,7 @@ foreach my $account (sort keys %$accounts) {
$states{'mfa_totp_configured'} = OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::MFA_TOTP_CONFIGURED_GROUP) ? 1 : 0;
$states{'mfa_totp_bypass'} = OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::MFA_TOTP_BYPASS_GROUP) ? 1 : 0;
$states{'pam_auth_bypass'} = OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::PAM_AUTH_BYPASS_GROUP) ? 1 : 0;
$states{'mfa_any'} = OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::MFA_ANY_GROUP) ? 1 : 0;
if ($fnretPassword) {
$states{"password_$_"} = $fnretPassword->value->{$account}{$_} for (keys %{$fnretPassword->value->{$account}});
@ -159,7 +160,7 @@ foreach my $account (sort keys %$accounts) {
push @mfaTOTP, 'bypass' if $states{'mfa_totp_bypass'};
osh_info sprintf(
"%-18s %6d active:%-12s expired:%-12s can_connect:%-12s already_seen:%-12s mfa_password:%-25s mfa_totp:%-25s pam_bypass:%-12s pass_status:%-15s pass_changed:%-10s pass_min_days:%-3d pass_max_days:%-3d pass_warn_days:%-3d %s\n",
"%-18s %6d active:%-12s expired:%-12s can_connect:%-12s already_seen:%-12s mfa_password:%-25s mfa_totp:%-25s pam_bypass:%-12s mfa_any:%-12s pass_status:%-15s pass_changed:%-10s pass_min_days:%-3d pass_max_days:%-3d pass_warn_days:%-3d %s\n",
$account,
$accounts->{$account}{'uid'},
tristate2str($states{'is_active'}),
@ -169,6 +170,7 @@ foreach my $account (sort keys %$accounts) {
@mfaPassword ? colored(join(',', @mfaPassword), 'green') : colored('-', 'blue'),
@mfaTOTP ? colored(join(',', @mfaTOTP), 'green') : colored('-', 'blue'),
tristate2str($states{'pam_auth_bypass'}, 1),
tristate2str($states{'mfa_any'}, 1),
(
$states{'password_password'} eq 'locked'
? colored('locked', 'blue')

View file

@ -23,6 +23,7 @@ my $remainingOptions = OVH::Bastion::Plugin::begin(
"idle-ignore=s" => \$modify{'idle-ignore'},
"max-inactive-days=i" => \$modify{'max-inactive-days'},
"osh-only=s" => \$modify{'osh-only'},
"mfa-any=s" => \$modify{'mfa-any'},
},
helptext => <<'EOF',
Modify an account configuration
@ -54,6 +55,11 @@ Usage: --osh SCRIPT_NAME --account ACCOUNT [--option value [--option value [...]
Setting this option to zero disables account expiration. Setting this option to -1 removes this account
expiration policy, i.e. the global bastion setting will apply.
--osh-only yes|no If enabled, this account can only use ``--osh`` commands, and can't connect anywhere through the bastion
--mfa-any yes|no Control the ingress login requirements for pubkey and pam (when a password and/or TOTP is set).
When disabled, the user needs pubkey AND pam, this is the default.
When enabled, the user can authenticate with either pubkey OR pam.
If the account has no password/TOTP, this option has no effect, i.e: pubkey is used. Egress is not affected.
EOF
);
@ -80,7 +86,7 @@ foreach my $key (qw{ mfa-password-required mfa-totp-required }) {
osh_exit 'ERR_INVALID_PARAMETER', "Expected '--$key yes' or '--$key no' or '--$key bypass' instead of '--$key $modify{$key}'";
}
}
foreach my $key (qw{ always-active pam-auth-bypass idle-ignore osh-only }) {
foreach my $key (qw{ always-active pam-auth-bypass idle-ignore osh-only mfa-any }) {
next unless $modify{$key};
if (not grep { $modify{$key} eq $_ } qw{ yes no }) {
help();

View file

@ -1,13 +1,13 @@
{
"master_only": true,
"interactive": [
"accountModify" , {"ac": ["--account"]},
"accountModify --account" , {"ac": ["<ACCOUNT>"]},
"accountModify --account \\S+" , {"ac": ["--mfa-password-required","--mfa-totp-required","--pam-auth-bypass","--always-active","--egress-strict-host-key-checking","--personal-egress-mfa-required","--idle-ignore"]},
"accountModify --account \\S+ .*(--mfa-password-required|--mfa-totp-required)" , {"ac": ["yes","no","bypass"]},
"accountModify --account \\S+ .*(--pam-auth-bypass|--mfa-auth-bypass|--always-active|idle-ignore)", {"ac": ["yes","no"]},
"accountModify --account \\S+ .*(--egress-strict-host-key-checking)" , {"ac": ["yes","accept-new","no","ask","default","bypass"]},
"accountModify --account \\S+ .*(--personal-egress-mfa-required)" , {"ac": ["password","totp","any","none"]},
"accountModify --account \\S+ .*(yes|accept-new|no|bypass|ask|default|totp|password|none)" , {"ac": ["--mfa-password-required","--mfa-totp-required","--pam-auth-bypass","--always-active","--egress-strict-host-key-checking","--personal-egress-mfa-required","--idle-ignore","<enter>"]}
"accountModify" , {"ac": ["--account"]},
"accountModify --account" , {"ac": ["<ACCOUNT>"]},
"accountModify --account \\S+" , {"ac": ["--mfa-password-required","--mfa-totp-required","--pam-auth-bypass","--always-active","--egress-strict-host-key-checking","--personal-egress-mfa-required","--idle-ignore","--mfa-any"]},
"accountModify --account \\S+ .*(--mfa-password-required|--mfa-totp-required)" , {"ac": ["yes","no","bypass"]},
"accountModify --account \\S+ .*(--pam-auth-bypass|--mfa-auth-bypass|--always-active|idle-ignore|--mfa-any)", {"ac": ["yes","no"]},
"accountModify --account \\S+ .*(--egress-strict-host-key-checking)" , {"ac": ["yes","accept-new","no","ask","default","bypass"]},
"accountModify --account \\S+ .*(--personal-egress-mfa-required)" , {"ac": ["password","totp","any","none"]},
"accountModify --account \\S+ .*(yes|accept-new|no|bypass|ask|default|totp|password|none)" , {"ac": ["--mfa-password-required","--mfa-totp-required","--pam-auth-bypass","--always-active","--egress-strict-host-key-checking","--personal-egress-mfa-required","--idle-ignore","-mfa-any","<enter>"]}
]
}

View file

@ -35,6 +35,8 @@ Output example
~ - Additional TOTP authentication is not required for this account
~ - Additional TOTP authentication bypass is disabled for this account
~ - Additional TOTP authentication is disabled
~ - PAM authentication bypass is disabled
~ - Alternative authentication logic (allow both pubkey alone and PAM alone) is disabled
~ - MFA policy on personal accesses (using personal keys) on egress side is: password
~ Account PAM UNIX password information (used for password MFA):

View file

@ -129,10 +129,11 @@ UsePAM yes
# Unconditionally skip PAM auth for members of the bastion-nopam group
Match Group bastion-nopam
AuthenticationMethods publickey
# if in one of the mfa groups, use pam
Match Group mfa-totp-configd
AuthenticationMethods publickey,keyboard-interactive:pam
Match Group mfa-password-configd
# if in one of the mfa groups AND the mfa-any group, use publickey OR pam
Match Group mfa-totp-configd,mfa-password-configd Group mfa-any
AuthenticationMethods publickey keyboard-interactive:pam
# if in one of the mfa groups, use publickey AND pam
Match Group mfa-totp-configd,mfa-password-configd
AuthenticationMethods publickey,keyboard-interactive:pam
# by default, always ask the publickey (no PAM)
Match All

View file

@ -129,10 +129,11 @@ UsePAM yes
# Unconditionally skip PAM auth for members of the bastion-nopam group
Match Group bastion-nopam
AuthenticationMethods publickey
# if in one of the mfa groups, use pam
Match Group mfa-totp-configd
AuthenticationMethods publickey,keyboard-interactive:pam
Match Group mfa-password-configd
# if in one of the mfa groups AND the mfa-any group, use publickey OR pam
Match Group mfa-totp-configd,mfa-password-configd Group mfa-any
AuthenticationMethods publickey keyboard-interactive:pam
# if in one of the mfa groups, use publickey AND pam
Match Group mfa-totp-configd,mfa-password-configd
AuthenticationMethods publickey,keyboard-interactive:pam
# by default, always ask the publickey (no PAM)
Match All

View file

@ -133,10 +133,11 @@ UsePAM yes
# Unconditionally skip PAM auth for members of the bastion-nopam group
Match Group bastion-nopam
AuthenticationMethods publickey
# if in one of the mfa groups, use pam
Match Group mfa-totp-configd
AuthenticationMethods publickey,keyboard-interactive:pam
Match Group mfa-password-configd
# if in one of the mfa groups AND the mfa-any group, use publickey OR pam
Match Group mfa-totp-configd,mfa-password-configd Group mfa-any
AuthenticationMethods publickey keyboard-interactive:pam
# if in one of the mfa groups, use publickey AND pam
Match Group mfa-totp-configd,mfa-password-configd
AuthenticationMethods publickey,keyboard-interactive:pam
# by default, always ask the publickey (no PAM)
Match All

View file

@ -133,10 +133,11 @@ UsePAM yes
# Unconditionally skip PAM auth for members of the bastion-nopam group
Match Group bastion-nopam
AuthenticationMethods publickey
# if in one of the mfa groups, use pam
Match Group mfa-totp-configd
AuthenticationMethods publickey,keyboard-interactive:pam
Match Group mfa-password-configd
# if in one of the mfa groups AND the mfa-any group, use publickey OR pam
Match Group mfa-totp-configd,mfa-password-configd Group mfa-any
AuthenticationMethods publickey keyboard-interactive:pam
# if in one of the mfa groups, use publickey AND pam
Match Group mfa-totp-configd,mfa-password-configd
AuthenticationMethods publickey,keyboard-interactive:pam
# by default, always ask the publickey (no PAM)
Match All

View file

@ -136,10 +136,11 @@ UsePAM yes
# Unconditionally skip PAM auth for members of the bastion-nopam group
Match Group bastion-nopam
AuthenticationMethods publickey
# if in one of the mfa groups, use pam
Match Group mfa-totp-configd
AuthenticationMethods publickey,keyboard-interactive:pam
Match Group mfa-password-configd
# if in one of the mfa groups AND the mfa-any group, use publickey OR pam
Match Group mfa-totp-configd,mfa-password-configd Group mfa-any
AuthenticationMethods publickey keyboard-interactive:pam
# if in one of the mfa groups, use publickey AND pam
Match Group mfa-totp-configd,mfa-password-configd
AuthenticationMethods publickey,keyboard-interactive:pam
# by default, always ask the publickey (no PAM)
Match All

View file

@ -136,10 +136,11 @@ UsePAM yes
# Unconditionally skip PAM auth for members of the bastion-nopam group
Match Group bastion-nopam
AuthenticationMethods publickey
# if in one of the mfa groups, use pam
Match Group mfa-totp-configd
AuthenticationMethods publickey,keyboard-interactive:pam
Match Group mfa-password-configd
# if in one of the mfa groups AND the mfa-any group, use publickey OR pam
Match Group mfa-totp-configd,mfa-password-configd Group mfa-any
AuthenticationMethods publickey keyboard-interactive:pam
# if in one of the mfa groups, use publickey AND pam
Match Group mfa-totp-configd,mfa-password-configd
AuthenticationMethods publickey,keyboard-interactive:pam
# by default, always ask the publickey (no PAM)
Match All

View file

@ -126,10 +126,11 @@ UsePAM yes
# Unconditionally skip PAM auth for members of the bastion-nopam group
Match Group bastion-nopam
AuthenticationMethods publickey
# if in one of the mfa groups, use pam
Match Group mfa-totp-configd
AuthenticationMethods publickey,keyboard-interactive:pam
Match Group mfa-password-configd
# if in one of the mfa groups AND the mfa-any group, use publickey OR pam
Match Group mfa-totp-configd,mfa-password-configd Group mfa-any
AuthenticationMethods publickey keyboard-interactive:pam
# if in one of the mfa groups, use publickey AND pam
Match Group mfa-totp-configd,mfa-password-configd
AuthenticationMethods publickey,keyboard-interactive:pam
# by default, always ask the publickey (no PAM)
Match All

View file

@ -103,6 +103,7 @@ use constant {
MFA_TOTP_CONFIGURED_GROUP => 'mfa-totp-configd',
MFA_TOTP_BYPASS_GROUP => 'mfa-totp-bypass',
PAM_AUTH_BYPASS_GROUP => 'bastion-nopam',
MFA_ANY_GROUP => 'mfa-any',
TOTP_FILENAME => '.otp',
TOTP_BASEDIR => '/var/otp',

View file

@ -181,6 +181,7 @@ fi
a3=" $t ssh -F $mytmpdir/ssh_config -i $account3key1file $account3@$remote_ip -p $remote_port -- $js "
a4=" $t ssh -F $mytmpdir/ssh_config -i $account4key1file $account4@$remote_ip -p $remote_port -- $js "
a4f="$tf ssh -F $mytmpdir/ssh_config -i $account4key1file $account4@$remote_ip -p $remote_port -- $js "
a4np="$t ssh -F $mytmpdir/ssh_config -o PubkeyAuthentication=no $account4@$remote_ip -p $remote_port -- $js "
r0=" $t ssh -F $mytmpdir/ssh_config -i $rootkeyfile root@$remote_ip -p $remote_port -- "
};

View file

@ -381,6 +381,76 @@ testsuite_mfa()
success a0_remove_mfa_req_a4_dupe $a0 --osh accountModify --account $account4 --pam-auth-bypass no --mfa-totp-required no --mfa-password-required no
json .error_code OK .command accountModify .value.pam_auth_bypass.error_code OK_NO_CHANGE .value.mfa_totp_required.error_code OK_NO_CHANGE .value.mfa_password_required.error_code OK_NO_CHANGE
# remove totp from account4 to simplify the following mfa-any tests
grant accountMFAResetTOTP
success mfa a0_nototp_a4 $a0 --osh accountMFAResetTOTP --account $account4
json .command accountMFAResetTOTP .error_code OK
revoke accountMFAResetTOTP
# no mfa-any, success with pubkey and password
script mfa a4_no_mfaany_login_pubkey_pam "echo 'set timeout 30; \
spawn $a4 --osh groupList; \
expect \"word:\" { sleep 0.2; send \"$a4_password\\n\"; }; \
expect eof; \
lassign [wait] pid spawnid value value; \
exit \$value' | expect -f -"
retvalshouldbe 0
contain 'Multi-Factor Authentication enabled, an additional authentication factor is required (password).'
contain REGEX 'Password:|Password for'
json .command groupList .error_code OK_EMPTY
# no mfa-any, fail with pubkey but no password (timeout)
script mfa a4_no_mfaany_login_pubkey_nopam $a4 --osh groupList
retvalshouldbe 124
contain 'Multi-Factor Authentication enabled, an additional authentication factor is required (password).'
contain 'Your password expires on'
contain 'in 89 days'
contain REGEX 'Password:|Password for'
nocontain 'JSON_OUTPUT'
# no mfa-any, fail with no pubkey (never gets to ask for the password)
script mfa a4_no_mfaany_login_nopubkey_pam $a4np --osh groupList
retvalshouldbe 255
contain 'Permission denied (publickey).'
nocontain 'password'
nocontain 'JSON_OUTPUT'
# set mfa-any on account4
success mfa a0_set_mfaany_a4 $a0 --osh accountModify --account $account4 --mfa-any yes
json .error_code OK .command accountModify .value.mfa_any.error_code OK
# set mfa-any on account4 (dupe)
success mfa a0_set_mfaany_a4_dupe $a0 --osh accountModify --account $account4 --mfa-any yes
json .error_code OK .command accountModify .value.mfa_any.error_code OK_NO_CHANGE
# success with pubkey but no password
success mfa a4_mfaany_login_pubkey_nopam $a4 --osh groupList
json .command groupList .error_code OK_EMPTY
# success with password but no pubkey
script mfa a4_mfaany_login_nopubkey_pam "echo 'set timeout 30; \
spawn $a4np --osh groupList; \
expect \"word:\" { sleep 0.2; send \"$a4_password\\n\"; }; \
expect eof; \
lassign [wait] pid spawnid value value; \
exit \$value' | expect -f -"
retvalshouldbe 0
contain 'Multi-Factor Authentication enabled, an additional authentication factor is required (password).'
contain REGEX 'Password:|Password for'
json .command groupList .error_code OK_EMPTY
# unset mfa-any on account4
success mfa a0_unset_mfaany_a4 $a0 --osh accountModify --account $account4 --mfa-any no
json .error_code OK .command accountModify .value.mfa_any.error_code OK
# unset mfa-any on account4 (dupe)
success mfa a0_unset_mfaany_a4_dupe $a0 --osh accountModify --account $account4 --mfa-any no
json .error_code OK .command accountModify .value.mfa_any.error_code OK_NO_CHANGE
# FIXME
# # reset totp
# script mfa a4_reset_totp "echo 'set timeout 30; \