the-bastion/bin/helper/osh-accountCreate
Antoine Leblanc 1c8efa6590 fix: osh-accountCreate: fix typo
Signed-off-by: Antoine Leblanc <antoine.leblanc@ovhcloud.com>
2021-12-31 16:22:03 +01:00

440 lines
17 KiB
Perl
Executable file

#! /usr/bin/perl -T
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
# NEEDGROUP osh-accountCreate
# SUDOERS %osh-accountCreate ALL=(root) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountCreate --type normal *
# FILEMODE 0700
# FILEOWN 0 0
#>HEADER
use common::sense;
use Getopt::Long qw(:config no_auto_abbrev no_ignore_case);
use Sys::Hostname ();
use JSON;
use POSIX ();
use File::Basename;
use lib dirname(__FILE__) . '/../../lib/perl';
use OVH::Bastion;
use OVH::Bastion::Helper;
# Fetch command options
my $fnret;
my ($result, @optwarns);
my ($type, $account, $realmFrom, $uid, @pubKeys, $comment, $alwaysActive, $uidAuto, $oshOnly, $maxInactiveDays, $immutableKey, $ttl);
eval {
local $SIG{__WARN__} = sub { push @optwarns, shift };
$result = GetOptions(
"type=s" => sub { $type //= $_[1] },
"from=s" => sub { $realmFrom //= $_[1] },
"uid=s" => sub { $uid //= $_[1] },
"account=s" => sub { $account //= $_[1] },
"always-active" => sub { $alwaysActive //= $_[1] },
"pubKey=s" => \@pubKeys,
"comment=s" => sub { $comment //= $_[1] },
'uid-auto' => sub { $uidAuto //= $_[1] },
'osh-only' => sub { $oshOnly //= $_[1] },
'max-inactive-days=i' => sub { $maxInactiveDays //= $_[1] },
'immutable-key' => sub { $immutableKey //= $_[1] },
'ttl=i' => sub { $ttl //= $_[1] },
);
};
if ($@) { die $@ }
OVH::Bastion::Helper::check_spurious_args();
if (!$result) {
local $" = ", ";
HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns");
}
if (!$account || !$type) {
HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'account' or 'type'");
}
#<HEADER
#>PARAMS:TYPE
osh_debug("Checking type");
if (not grep { $type eq $_ } qw{ normal realm }) {
HEXIT('ERR_INVALID_PARAMETER', "Expected type 'normal' or 'realm'");
}
#<PARAMS:TYPE
#>PARAMS:ACCOUNT
osh_debug("Checking account");
$fnret = OVH::Bastion::is_account_valid(account => $account);
$fnret or HEXIT($fnret);
# get returned untainted value
$account = $fnret->value->{'account'};
$fnret = OVH::Bastion::is_account_existing(account => $account);
$fnret->is_err and HEXIT($fnret);
$fnret->is_ok and HEXIT('KO_ALREADY_EXISTING', msg => "The account $account already exists");
$fnret = OVH::Bastion::is_group_existing(group => $account);
$fnret->is_err and HEXIT($fnret);
$fnret->is_ok and HEXIT('KO_ALREADY_EXISTING', msg => "The group $account already exists");
if ($type eq 'realm') {
$account = "realm_$account";
$fnret = OVH::Bastion::is_account_valid(account => $account, accountType => "realm");
$fnret or HEXIT($fnret);
$fnret = OVH::Bastion::is_account_existing(account => $account, accountType => "realm");
$fnret->is_err and HEXIT($fnret);
$fnret->is_ok and HEXIT('KO_ALREADY_EXISTING', msg => "The realm $account already exists");
$fnret = OVH::Bastion::is_group_existing(group => $account);
$fnret->is_err and HEXIT($fnret);
$fnret->is_ok and HEXIT('KO_ALREADY_EXISTING', msg => "The group $account already exists");
}
#<PARAMS:ACCOUNT
#>PARAMS:UID
if (not $uidAuto and not defined $uid) {
HEXIT('ERR_MISSING_PARAMETER', msg => "Missing one of 'uid-auto' or 'uid' argument");
}
if (defined $uid and $uidAuto) {
HEXIT('ERR_INCOMPATIBLE_PARAMETERS', msg => "Incompatible parameters 'uid' and 'uid-auto' specified");
}
if (defined $uid) {
$fnret = OVH::Bastion::is_valid_uid(uid => $uid, type => 'user');
$fnret or HEXIT($fnret);
$uid = $fnret->value;
getpwuid($uid) and HEXIT('ERR_UID_COLLISION', msg => "This UID ($uid) is already taken");
$fnret = OVH::Bastion::is_valid_uid(uid => $uid, type => 'group');
$fnret or HEXIT($fnret);
getgrgid($uid) and HEXIT('ERR_GID_COLLISION', msg => "This GID ($uid) is already taken");
}
elsif ($uidAuto) {
$fnret = OVH::Bastion::get_next_available_uid(available_gid => 1, available_gid_ttyrec => 1);
$fnret or HEXIT($fnret);
$uid = $fnret->value();
}
#<PARAMS:UID
if (defined $maxInactiveDays && $maxInactiveDays < 0) {
HEXIT('ERR_INVALID_PARAMETER', msg => "Expected a >= 0 amount of days for --max-inactive-days");
}
#>PARAMS
my $ttygroup = "$account-tty";
$fnret = OVH::Bastion::is_group_existing(group => $ttygroup);
$fnret and HEXIT('ERR_TTY_GROUP_ALREADY_EXISTS', msg => "The TTY group for this account ($ttygroup) already exists!");
#<PARAMS
#>RIGHTSCHECK
if ($self eq 'root') {
osh_debug "Real root, skipping checks of permissions";
}
else {
# need to perform another security check
if ($type eq 'realm') {
$fnret = OVH::Bastion::is_user_in_group(user => $self, group => "osh-realmCreate");
$fnret or HEXIT('ERR_SECURITY_VIOLATION', msg => "You're not allowed to run this, dear $self");
}
else {
$fnret = OVH::Bastion::is_user_in_group(user => $self, group => "osh-accountCreate");
$fnret or HEXIT('ERR_SECURITY_VIOLATION', msg => "You're not allowed to run this, dear $self");
}
}
#<RIGHTSCHECK
#>CODE
$fnret = OVH::Bastion::load_configuration();
$fnret or HEXIT($fnret);
my $config = $fnret->value;
my $ttygid = $uid + $config->{'ttyrecGroupIdOffset'};
getgrgid($ttygid) and HEXIT('ERR_GID_COLLISION', msg => "This GID ($ttygid) is already taken");
if ($uid < $config->{'accountUidMin'} or $uid > $config->{'accountUidMax'}) {
HEXIT('ERR_UID_INVALID_RANGE', msg => "UID must be < " . $config->{'accountUidMin'} . " and > " . $config->{'accountUidMax'});
}
my @vettedKeys;
foreach my $key (@pubKeys) {
$fnret = OVH::Bastion::is_valid_public_key(pubKey => $key, way => 'ingress');
$fnret or HEXIT($fnret);
$key = $fnret->value->{'typecode'} . ' ' . $fnret->value->{'base64'};
if ($fnret->value->{'comment'}) {
$key .= ' ' . $fnret->value->{'comment'};
}
push @vettedKeys, $key;
}
my $prefix = $fnret->value->{'prefix'};
my @userProvidedIpList = ();
if ($prefix) {
my ($fromString) = $prefix =~ m{from=["']([^"']+)["']};
if ($fromString) {
@userProvidedIpList = split /,/, $fromString;
}
}
$fnret = OVH::Bastion::get_from_for_user_key(userProvidedIpList => \@userProvidedIpList);
$fnret or HEXIT($fnret);
my $from = $fnret->value->{'from'};
my $ipList = $fnret->value->{'ipList'};
my $homedir = "/home/$account";
osh_info "Creating group $account with GID $uid...";
$fnret = OVH::Bastion::sys_groupadd(noisy_stderr => 1, gid => $uid, group => $account);
$fnret->err eq 'OK'
or HEXIT('ERR_GROUPADD_FAILED', msg => "Error while running groupadd with UID $uid and group $account (" . $fnret->msg . ")");
osh_debug('ok, group was created');
osh_info "Creating user $account with UID $uid...";
$fnret = OVH::Bastion::sys_useradd(
noisy_stderr => 1,
user => $account,
uid => $uid,
gid => $uid,
shell => $OVH::Bastion::BASEPATH . '/bin/shell/osh.pl',
home => $homedir
);
$fnret->err eq 'OK'
or HEXIT('ERR_USERADD_FAILED', msg => "Error while running useradd for $account UID/GID $uid (" . $fnret->msg . ")");
osh_debug('user created');
chmod 0750, $homedir;
mkdir $homedir . "/.ssh" if (!-d "$homedir/.ssh");
chmod 0750, $homedir . "/.ssh";
chown $uid, $uid, "$homedir/.ssh";
my $akfile = $homedir . '/' . OVH::Bastion::AK_FILE;
if (!OVH::Bastion::touch_file($akfile)) {
HEXIT('ERR_CANNOT_CREATE_FILE', msg => "Failed to create authorized_keys file");
}
chmod 0640, $akfile;
chown $uid, $uid, $akfile;
osh_info "Creating tty group of account...";
$fnret = OVH::Bastion::sys_groupadd(noisy_stderr => 1, group => $ttygroup, gid => $ttygid);
$fnret->err eq 'OK'
or HEXIT('ERR_GROUPADD_FAILED', msg => "Error while running groupadd with UID $ttygid and group $ttygroup (" . $fnret->msg . ")");
osh_debug('ok, group was created');
$fnret = OVH::Bastion::add_user_to_group(user => $account, group => $ttygroup, groupType => 'tty', accountType => ($type eq 'realm' ? 'realm' : 'normal'));
$fnret or HEXIT($fnret);
# adding account to bastion-users group
$fnret = OVH::Bastion::add_user_to_group(user => $account, group => "bastion-users", accountType => ($type eq 'realm' ? 'realm' : 'normal'));
$fnret or HEXIT($fnret);
if ($type ne 'realm') {
osh_info "Adding account to potential supplementary groups...";
if ($config->{'accountCreateSupplementaryGroups'}) {
foreach my $suppGroup (@{$config->{'accountCreateSupplementaryGroups'}}) {
$fnret = OVH::Bastion::add_user_to_group(user => $account, group => $suppGroup, groupType => 'osh');
if ($fnret) {
osh_info "Account added to group $suppGroup";
}
else {
osh_warn "Couldn't add account $account to group $suppGroup";
}
}
}
}
osh_info "Creating needed files and directories with proper permissions in home...";
my $ttyrecdir = $homedir . "/ttyrec";
mkdir $ttyrecdir;
if (!chown $uid, $uid, $ttyrecdir) {
HEXIT('ERR_CANNOT_CHOWN', msg => "Couldn't chown ttyrec directory ($!)");
}
if (!chmod 0700, $ttyrecdir) {
HEXIT('ERR_CANNOT_CHMOD', msg => "Couldn't chmod ttyrec directory ($!)");
}
osh_debug('applying an acl for group ' . $ttygroup);
OVH::Bastion::sys_setfacl(target => $ttyrecdir, clear => 1, perms => "g:$ttygroup:rX")
or HEXIT('ERR_SETFACL_FAILED', msg => "Error setting ACL on $ttyrecdir");
OVH::Bastion::sys_setfacl(target => $ttyrecdir, default => 1, perms => "g:$ttygroup:rX")
or HEXIT('ERR_SETFACL_FAILED', msg => "Error setting default ACL on $ttyrecdir");
OVH::Bastion::sys_setfacl(target => $homedir, clear => 1, perms => "g:$ttygroup:x,g:osh-auditor:x")
or HEXIT('ERR_SETFACL_FAILED', msg => "Error setting ACL on $homedir");
OVH::Bastion::sys_setfacl(target => "$homedir/.ssh", clear => 1, perms => "g:osh-auditor:x")
or HEXIT('ERR_SETFACL_FAILED', msg => "Error setting ACL on $homedir/.ssh");
osh_info "Creating some more directories...";
mkdir "/home/allowkeeper/$account";
OVH::Bastion::touch_file("/home/allowkeeper/$account/allowed.ip");
OVH::Bastion::touch_file("/home/allowkeeper/$account/allowed.private");
osh_info "Applying proper ownerships...";
$fnret = OVH::Bastion::execute(
cmd => ['chown', 'allowkeeper:allowkeeper', "/home/allowkeeper/$account", "/home/allowkeeper/$account/allowed.ip", "/home/allowkeeper/$account/allowed.private"],
noisy_stderr => 1
);
$fnret->err eq 'OK' or HEXIT('ERR_CHMOD_FAILED', msg => "Error while running chmod on allowkeeper (" . $fnret->msg . ")");
$fnret = OVH::Bastion::execute(cmd => ['chmod', '-R', 'o+rX', "/home/allowkeeper/$account"], noisy_stderr => 1);
$fnret->err eq 'OK' or HEXIT('ERR_CHMOD_FAILED', msg => "Error while running chmod -R on allowkeeper (" . $fnret->msg . ")");
if (ref $config->{'accountCreateDefaultPersonalAccesses'} eq 'ARRAY' && $type eq 'normal') {
foreach my $defAccess (@{$config->{'accountCreateDefaultPersonalAccesses'}}) {
my (undef, $user, $ip, undef, $port) = $defAccess =~ m{^(([^@]+)@)?([0-9./]+)(:(\d+))?$};
next unless $ip;
my @command = qw{ sudo -n -u allowkeeper -- };
push @command, $OVH::Bastion::BASEPATH . '/bin/helper/osh-accountModifyPersonalAccess';
push @command, '--target', 'any';
push @command, '--action', 'add';
push @command, '--account', $account;
push @command, '--ip', $ip;
if ($user) {
push @command, '--user', ($user eq 'ACCOUNT' ? $account : $user);
}
$port and push @command, '--port', $port;
$fnret = OVH::Bastion::execute(cmd => \@command, noisy_stdout => 1, noisy_stderr => 1, is_helper => 1);
$fnret->err eq 'OK' or osh_warn("Couldn't add private access to account to $defAccess (" . $fnret->msg . ")");
}
}
# store some metadata
my $creation_time = time();
my %metadata = (
by => $self,
bastion_version => $OVH::Bastion::VERSION,
datetime_utc => POSIX::strftime("%a %Y-%m-%d %H:%M:%S UTC", gmtime($creation_time)),
datetime_local => POSIX::strftime("%a %Y-%m-%d %H:%M:%S %Z", localtime($creation_time)),
timestamp => $creation_time,
comment => $comment,
);
$fnret = OVH::Bastion::account_config(account => $account, key => "creation_info", value => encode_json(\%metadata));
if (!$fnret) {
osh_warn("Couldn't set creation_info account metadata, continuing anyway");
warn_syslog("While creating account '$account', couldn't set creation_info metadata: " . $fnret->msg);
}
# also store the bare creation timestamp by itself, it's used by osh.pl for TTL accounts
$fnret = OVH::Bastion::account_config(account => $account, key => "creation_timestamp", value => time());
if (!$fnret) {
osh_warn("Couldn't store creation timestamp (" . $fnret->msg . "), continuing anyway");
warn_syslog("While creating account '$account', couldn't store creation timestamp: " . $fnret->msg);
}
if ($ttl) {
$fnret = OVH::Bastion::duration2human(seconds => $ttl);
osh_info sprintf("Setting this account TTL (will expire in %s)", $fnret->value->{'human'});
$fnret = OVH::Bastion::account_config(account => $account, key => "account_ttl", value => $ttl);
if (!$fnret) {
osh_warn("Couldn't store account TTL (" . $fnret->msg . "), this account will NOT expire, continuing anyway");
warn_syslog("Couldn't store account TTL (" . $fnret->msg . ") while creating account '$account', this account will NOT expire, continuing anyway");
}
}
if ($alwaysActive || $type eq 'realm') {
$fnret = OVH::Bastion::account_config(account => $account, key => OVH::Bastion::OPT_ACCOUNT_ALWAYS_ACTIVE, value => "yes", public => 1);
if (!$fnret) {
osh_warn("Couldn't store always_active flag (" . $fnret->msg . "), continuing anyway");
warn_syslog("Couldn't store always_active flag (" . $fnret->msg . ") while creating account '$account', continuing anyway");
}
}
$fnret = OVH::Bastion::add_user_to_group(user => "keyreader", group => $account, accountType => 'group', groupType => 'regular');
$fnret or HEXIT($fnret);
osh_debug('user keyreader added to group');
my $finalPrefix = $realmFrom ? sprintf('from="%s"', $realmFrom) : $from;
$finalPrefix .= ' ' if $finalPrefix;
osh_info "Adding provided public key in authorized_keys file...";
if (open(my $fh_keys, '>>', $akfile)) {
foreach my $key (@vettedKeys) {
print $fh_keys $finalPrefix . $key . "\n";
}
close($fh_keys);
}
else {
HEXIT("ERR_CANNOT_ADD_KEY", msg => "Couldn't open $akfile when trying to add provided public key");
}
# push this flag to prevent ssh/telnet usage
if ($oshOnly) {
$fnret = OVH::Bastion::account_config(account => $account, key => OVH::Bastion::OPT_ACCOUNT_OSH_ONLY, value => "yes");
$fnret or HEXIT($fnret);
}
# specific expiration policy. Note that 0 is a valid value (means "never").
if (defined $maxInactiveDays) {
$fnret = OVH::Bastion::account_config(account => $account, %{OVH::Bastion::OPT_ACCOUNT_MAX_INACTIVE_DAYS()}, value => $maxInactiveDays);
$fnret or HEXIT($fnret);
}
# chown to root so user can no longer touch it
if ($immutableKey) {
chown 0, -1, $akfile;
}
osh_info "Generating account personal bastion key...";
$fnret = OVH::Bastion::generate_ssh_key(
folder => "$homedir/.ssh",
prefix => 'private',
name => $account,
gid => $uid,
uid => $uid,
algo => OVH::Bastion::config('defaultAccountEgressKeyAlgorithm')->value,
size => OVH::Bastion::config('defaultAccountEgressKeySize')->value,
);
$fnret or HEXIT($fnret);
osh_info "Account successfully created!";
if ($realmFrom) {
osh_info "Realm will be able to connect from the following IPs: $realmFrom";
}
elsif (scalar(@$ipList) > 0) {
osh_info "Account will be able to connect from the following IPs: " . join(', ', @$ipList);
}
# allowed to sudo for the account
osh_info("Configuring sudoers for this account");
$fnret = OVH::Bastion::execute(cmd => [$OVH::Bastion::BASEPATH . '/bin/sudogen/generate-sudoers.sh', 'create', 'account', $account], must_succeed => 1, noisy_stdout => 1);
$fnret or HEXIT('ERR_CANNOT_CREATE_SUDOERS', msg => "An error occurred while creating sudoers for this account");
my $bastionName = $config->{'bastionName'};
my $bastionCommand = $config->{'bastionCommand'};
$bastionCommand =~ s/USER|ACCOUNT/$account/g;
$bastionCommand =~ s/CACHENAME|BASTIONNAME/$bastionName/g;
my $hostname = Sys::Hostname::hostname();
$bastionCommand =~ s/HOSTNAME/$hostname/g;
if ($type eq 'realm') {
osh_info "Realm has been created.";
}
else {
osh_info "==> alias $bastionName='$bastionCommand'";
osh_info "To test his access, ask this user to set the above alias in their .bash_aliases, then run `$bastionName --osh info'";
}
OVH::Bastion::syslogFormatted(
severity => 'info',
type => 'account',
fields => [
['action', 'create'],
['account', $account],
['account_uid', $uid],
['public_key', @vettedKeys ? $vettedKeys[0] : undef],
['always_active', ($alwaysActive ? 'true' : 'false')],
['uid_auto', ($uidAuto ? 'true' : 'false')],
['osh_only', ($oshOnly ? 'true' : 'false')],
['immutable_key', ($immutableKey ? 'true' : 'false')],
['comment', $comment],
]
);
HEXIT('OK');