Skip to content

Commit

Permalink
feat: add support for Duo PAM auth as MFA (#249)
Browse files Browse the repository at this point in the history
  • Loading branch information
speed47 committed Oct 15, 2021
1 parent d85298f commit 447af53
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 34 deletions.
26 changes: 21 additions & 5 deletions bin/helper/osh-accountMFAResetTOTP
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,31 @@ if (OVH::Bastion::is_admin(account => $account, sudo => 1) && !OVH::Bastion::is_
HEXIT('ERR_SECURITY_VIOLATION', msg => "You can't reset the TOTP of an admin without being admin yourself");
}

my $TOTPProvider = OVH::Bastion::config("TOTPProvider")->value;
if ($TOTPProvider eq 'none') {
HEXIT('ERR_CONFIGURATION_ERROR', msg => "TOTP Provider has not been set, please report to your sysadmin");
}
elsif ($TOTPProvider eq 'google-authenticator') {

# for google-authenticator, attempt remove the .otp file (non-fatal)
if (!unlink($home . '/' . OVH::Bastion::TOTP_GAUTH_FILENAME)) {
warn_syslog("Couldn't remove the TOTP file ($!), this is not fatal, continuing anyway");
}
}
elsif ($TOTPProvider eq 'duo') {

# duo doesn't need any user-specific local cleanup
}
else {
# unknown provider, this shouldn't happen
HEXIT('ERR_CONFIGURATION_ERROR', msg => "An unknown TOTP provider has been provided, please check with your sysadmin.");
}

# remove the user from the TOTP configured group
if (OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::MFA_TOTP_CONFIGURED_GROUP)) {
$fnret = OVH::Bastion::sys_delmemberfromgroup(user => $account, group => OVH::Bastion::MFA_TOTP_CONFIGURED_GROUP);
$fnret or HEXIT($fnret);
}

# remove the .otp file (non-fatal)
if (!unlink($home . '/' . OVH::Bastion::TOTP_FILENAME)) {
osh_warn("Couldn't remove the TOTP file ($!), this is not fatal, continuing anyway");
}

osh_info "TOTP has been reset, " . ($account eq $self ? 'you' : $account) . " can re-enroll by using the `--osh selfMFASetupTOTP' command, if applicable";
HEXIT('OK');
64 changes: 39 additions & 25 deletions bin/plugin/open/selfMFASetupTOTP
Original file line number Diff line number Diff line change
Expand Up @@ -37,39 +37,53 @@ if ($ENV{'OSH_IN_INTERACTIVE_SESSION'}) {

# do the TOTP enrollment

# first, check if the google-authenticator we have supports --issuer, if not, just omit it, it's not a deal-breaker
$fnret = OVH::Bastion::execute(cmd => ['google-authenticator', '-h'], must_succeed => 1);
$fnret or HEXIT($fnret);
my @additional_params;
if (grep { /--issuer/ } @{$fnret->value->{'stdout'}}) {
push @additional_params, "--issuer=" . OVH::Bastion::config('bastionName')->value;
}
if ($noConfirm && grep { /--no-confirm/ } @{$fnret->value->{'stdout'}}) {
push @additional_params, "--no-confirm";
}
my $TOTPProvider = OVH::Bastion::config("TOTPProvider")->value;

@command = (
'script', '-q', '-c', "google-authenticator -f -t -Q UTF8 -r 3 -R 15 -w 2 -D " . join(" ", @additional_params) . " -l $self -s $HOME/" . OVH::Bastion::TOTP_FILENAME,
'/dev/null'
);
{
local $ENV{'SHELL'} = '/bin/sh';
$fnret = OVH::Bastion::execute(cmd => \@command, noisy_stderr => 1, noisy_stdout => 1, expects_stdin => 1, is_binary => 1, must_succeed => 1);
if ($TOTPProvider eq 'none') {

# unconfigured: as we don't know which provider we have, just error out
osh_exit(R('ERR_CONFIGURATION_ERROR', msg => "No TOTP provider has been enabled, please check with your sysadmin if this is unexpected"));
}
$fnret or osh_exit $fnret;
elsif ($TOTPProvider eq 'google-authenticator') {

# first, check if the google-authenticator we have supports --issuer, if not, just omit it, it's not a deal-breaker
$fnret = OVH::Bastion::execute(cmd => ['google-authenticator', '-h'], must_succeed => 1);
$fnret or osh_exit($fnret);
my @additional_params;
if (grep { /--issuer/ } @{$fnret->value->{'stdout'}}) {
push @additional_params, "--issuer=" . OVH::Bastion::config('bastionName')->value;
}
if ($noConfirm && grep { /--no-confirm/ } @{$fnret->value->{'stdout'}}) {
push @additional_params, "--no-confirm";
}

if (!$fnret) {
osh_exit('ERR_TOTP_SETUP_FAILED', msg => "Couldn't setup TOTP for your account, try again!");
@command = (
'script', '-q', '-c', "google-authenticator -f -t -Q UTF8 -r 3 -R 15 -w 2 -D " . join(" ", @additional_params) . " -l $self -s $HOME/" . OVH::Bastion::TOTP_GAUTH_FILENAME,
'/dev/null'
);
{
local $ENV{'SHELL'} = '/bin/sh';
$fnret = OVH::Bastion::execute(cmd => \@command, noisy_stderr => 1, noisy_stdout => 1, expects_stdin => 1, is_binary => 1, must_succeed => 1);
}

if (!$fnret) {
osh_exit('ERR_TOTP_SETUP_FAILED', msg => "Couldn't setup TOTP for your account, try again!");
}

chmod 0400, $HOME . '/' . OVH::Bastion::TOTP_GAUTH_FILENAME;
}
elsif ($TOTPProvider eq 'duo') {

chmod 0400, $HOME . '/' . OVH::Bastion::TOTP_FILENAME;
# nothing to do locally, appart from marking the user as TOTP-active, which is done after this block.
}
else {
# unknown provider, this shouldn't happen
osh_exit(R('ERR_CONFIGURATION_ERROR', msg => "An unknown TOTP provider has been provided, please check with your sysadmin."));
}

# it worked, add the account to the proper system group
@command = qw{ sudo -n -u root -- /usr/bin/env perl -T };
push @command, $OVH::Bastion::BASEPATH . '/bin/helper/osh-selfMFASetupTOTP';
push @command, '--account', $self;

$fnret = OVH::Bastion::helper(cmd => \@command);
$fnret or osh_exit $fnret;

osh_exit $fnret;
osh_exit(OVH::Bastion::helper(cmd => \@command));
8 changes: 8 additions & 0 deletions etc/bastion/bastion.conf.dist
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,14 @@
# EXAMPLE: ["sudo","-n","-u","root","--","/sbin/pam_tally2","-u","%ACCOUNT%","-r"] or ["/usr/sbin/faillock","--reset"]
"MFAPostCommand": [],
#
# TOTPProvider (string)
# DESC: Defines which is the provider of the TOTP MFA, that will be used for the ``(self|account)MFA(Setup|Reset)TOTP`` commands. Allowed values are:
# - none: no TOTP provider is defined, the corresponding setup commands won't be available.
# - google-authenticator: the pam_google_authenticator.so module will be used, along with its corresponding setup binary. This is the default, for backward compatibility reasons. This is also what is configured in the provided pam templates.
# - 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.
Expand Down
4 changes: 2 additions & 2 deletions lib/perl/OVH/Bastion.pm
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ use constant {
PAM_AUTH_BYPASS_GROUP => 'bastion-nopam',
OSH_PUBKEY_AUTH_OPTIONAL_GROUP => 'osh-pubkey-auth-optional',

TOTP_FILENAME => '.otp',
TOTP_BASEDIR => '/var/otp',
TOTP_GAUTH_FILENAME => '.otp',
TOTP_BASEDIR => '/var/otp',

# authorized_keys file, relative to the user's HOME directory.
# if you change this, also change it in lib/shell/functions.inc
Expand Down
5 changes: 3 additions & 2 deletions lib/perl/OVH/Bastion/configuration.inc
Original file line number Diff line number Diff line change
Expand Up @@ -286,8 +286,9 @@ sub load_configuration {

# 4/6) Strings that must be one item of a specific enum.
foreach my $o (
{name => 'defaultAccountEgressKeyAlgorithm', default => 'rsa', valid => [qw{ rsa ecdsa ed25519 }]},
{name => 'accountMFAPolicy', default => 'enabled', valid => [qw{ disabled enabled password-required totp-required any-required }]},
{name => 'defaultAccountEgressKeyAlgorithm', default => 'rsa', valid => [qw{ rsa ecdsa ed25519 }]},
{name => 'accountMFAPolicy', default => 'enabled', valid => [qw{ disabled enabled password-required totp-required any-required }]},
{name => 'TOTPProvider', default => 'google-authenticator', valid => [qw{ none google-authenticator duo }]},
)
{
# if not defined, set to default value
Expand Down

0 comments on commit 447af53

Please sign in to comment.