the-bastion/bin/helper/osh-accountCreate

490 lines
18 KiB
Text
Raw Normal View History

2020-10-16 00:32:37 +08:00
#! /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
2020-10-16 00:32:37 +08:00
#>HEADER
use common::sense;
use Getopt::Long qw(:config no_auto_abbrev no_ignore_case);
2020-10-16 00:32:37 +08:00
use Sys::Hostname ();
use JSON;
use POSIX ();
2020-10-16 00:32:37 +08:00
use File::Basename;
use lib dirname(__FILE__) . '/../../lib/perl';
use OVH::Bastion;
2021-11-30 19:20:28 +08:00
use OVH::Bastion::Helper;
2020-10-16 00:32:37 +08:00
# Fetch command options
my $fnret;
my ($result, @optwarns);
2022-06-30 21:00:29 +08:00
my (
$forceKeyFrom, $type, $account, $realmFrom, $uid,
@pubKeys, $comment, $alwaysActive, $uidAuto, $oshOnly,
$maxInactiveDays, $immutableKey, $ttl
);
2020-10-16 00:32:37 +08:00
eval {
local $SIG{__WARN__} = sub { push @optwarns, shift };
$result = GetOptions(
"force-key-from=s" => sub { $forceKeyFrom //= $_[1] }, # only to be used by the install script
"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] },
2020-10-16 00:32:37 +08:00
);
};
if ($@) { die $@ }
OVH::Bastion::Helper::check_spurious_args();
2020-10-16 00:32:37 +08:00
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");
2020-10-16 00:32:37 +08:00
$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);
2020-10-16 00:32:37 +08:00
$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");
}
2020-10-16 00:32:37 +08:00
#>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'}) {
2022-06-30 21:00:29 +08:00
HEXIT('ERR_UID_INVALID_RANGE',
msg => "UID must be < " . $config->{'accountUidMin'} . " and > " . $config->{'accountUidMax'});
2020-10-16 00:32:37 +08:00
}
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;
}
}
2022-06-30 21:00:29 +08:00
$fnret = OVH::Bastion::get_from_for_user_key(
userProvidedIpList => \@userProvidedIpList,
forcedList => ($forceKeyFrom ? [$forceKeyFrom] : [])
);
2020-10-16 00:32:37 +08:00
$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'
2022-06-30 21:00:29 +08:00
or HEXIT('ERR_GROUPADD_FAILED',
msg => "Error while running groupadd with UID $uid and group $account (" . $fnret->msg . ")");
2020-10-16 00:32:37 +08:00
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)) {
2020-10-16 00:32:37 +08:00
HEXIT('ERR_CANNOT_CREATE_FILE', msg => "Failed to create authorized_keys file");
}
chmod 0640, $akfile;
chown $uid, $uid, $akfile;
2020-10-16 00:32:37 +08:00
osh_info "Creating tty group of account...";
$fnret = OVH::Bastion::sys_groupadd(noisy_stderr => 1, group => $ttygroup, gid => $ttygid);
$fnret->err eq 'OK'
2022-06-30 21:00:29 +08:00
or HEXIT('ERR_GROUPADD_FAILED',
msg => "Error while running groupadd with UID $ttygid and group $ttygroup (" . $fnret->msg . ")");
2020-10-16 00:32:37 +08:00
osh_debug('ok, group was created');
2022-06-30 21:00:29 +08:00
$fnret = OVH::Bastion::add_user_to_group(
user => $account,
group => $ttygroup,
groupType => 'tty',
accountType => ($type eq 'realm' ? 'realm' : 'normal')
);
2020-10-16 00:32:37 +08:00
$fnret or HEXIT($fnret);
# adding account to bastion-users group
2022-06-30 21:00:29 +08:00
$fnret = OVH::Bastion::add_user_to_group(
user => $account,
group => "bastion-users",
accountType => ($type eq 'realm' ? 'realm' : 'normal')
);
2020-10-16 00:32:37 +08:00
$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(
2022-06-30 21:00:29 +08:00
cmd => [
'chown', 'allowkeeper:allowkeeper',
"/home/allowkeeper/$account", "/home/allowkeeper/$account/allowed.ip",
"/home/allowkeeper/$account/allowed.private"
],
2020-10-16 00:32:37 +08:00
noisy_stderr => 1
);
2022-06-30 21:00:29 +08:00
$fnret->err eq 'OK'
or HEXIT('ERR_CHMOD_FAILED', msg => "Error while running chmod on allowkeeper (" . $fnret->msg . ")");
2020-10-16 00:32:37 +08:00
$fnret = OVH::Bastion::execute(cmd => ['chmod', '-R', 'o+rX', "/home/allowkeeper/$account"], noisy_stderr => 1);
2022-06-30 21:00:29 +08:00
$fnret->err eq 'OK'
or HEXIT('ERR_CHMOD_FAILED', msg => "Error while running chmod -R on allowkeeper (" . $fnret->msg . ")");
2020-10-16 00:32:37 +08:00
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;
2020-10-16 00:32:37 +08:00
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);
2020-10-16 00:32:37 +08:00
}
# also store the bare creation timestamp by itself, it's used by osh.pl for TTL accounts
2020-10-16 00:32:37 +08:00
$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);
2020-10-16 00:32:37 +08:00
}
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");
2022-06-30 21:00:29 +08:00
warn_syslog("Couldn't store account TTL ("
. $fnret->msg
. ") while creating account '$account', this account will NOT expire, continuing anyway");
2020-10-16 00:32:37 +08:00
}
}
if ($alwaysActive || $type eq 'realm') {
2022-06-30 21:00:29 +08:00
$fnret = OVH::Bastion::account_config(
account => $account,
key => OVH::Bastion::OPT_ACCOUNT_ALWAYS_ACTIVE,
value => "yes",
public => 1
);
2020-10-16 00:32:37 +08:00
if (!$fnret) {
osh_warn("Couldn't store always_active flag (" . $fnret->msg . "), continuing anyway");
2022-06-30 21:00:29 +08:00
warn_syslog("Couldn't store always_active flag ("
. $fnret->msg
. ") while creating account '$account', continuing anyway");
2020-10-16 00:32:37 +08:00
}
}
2022-06-30 21:00:29 +08:00
$fnret = OVH::Bastion::add_user_to_group(
user => "keyreader",
group => $account,
accountType => 'group',
groupType => 'regular'
);
2020-10-16 00:32:37 +08:00
$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)) {
2020-10-16 00:32:37 +08:00
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");
2020-10-16 00:32:37 +08:00
}
# push this flag to prevent ssh/telnet usage
if ($oshOnly) {
2022-06-30 21:00:29 +08:00
$fnret =
OVH::Bastion::account_config(account => $account, key => OVH::Bastion::OPT_ACCOUNT_OSH_ONLY, value => "yes");
2020-10-16 00:32:37 +08:00
$fnret or HEXIT($fnret);
}
# specific expiration policy. Note that 0 is a valid value (means "never").
if (defined $maxInactiveDays) {
2022-06-30 21:00:29 +08:00
$fnret = OVH::Bastion::account_config(
account => $account,
%{OVH::Bastion::OPT_ACCOUNT_MAX_INACTIVE_DAYS()}, value => $maxInactiveDays
);
$fnret or HEXIT($fnret);
}
2020-10-16 00:32:37 +08:00
# chown to root so user can no longer touch it
if ($immutableKey) {
chown 0, -1, $akfile;
2020-10-16 00:32:37 +08:00
}
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");
2022-06-30 21:00:29 +08:00
$fnret = OVH::Bastion::execute(
cmd => [$OVH::Bastion::BASEPATH . '/bin/sudogen/generate-sudoers.sh', 'create', 'account', $account],
must_succeed => 1,
noisy_stdout => 1
);
2020-10-16 00:32:37 +08:00
$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'";
2022-06-30 21:00:29 +08:00
osh_info("To test his access, ask this user to set the above alias in their .bash_aliases, ",
"then run `$bastionName --osh info'");
2020-10-16 00:32:37 +08:00
}
OVH::Bastion::syslogFormatted(
severity => 'info',
type => 'account',
fields => [
feat: revamp logs All connections and plugin executions emit two logs, an 'open' and a 'close' log. We now add all the details of the connection to the 'close' logs, those that were previously only available in the corresponding 'open' log. This way, it is no longer required to correlate both logs with their uniqid to have all the data: the 'close' log should suffice. The 'open' log is still there if for some reason the 'close' log can't be emitted (kill -9, system crash, etc.), or if the 'open' and the 'close' log are several hours, days or months appart. An additional field "duration" has been added to the 'close' logs, this represents the number of seconds (with millisecond precision) the connection lasted. Two new fields "globalsql" and "accountsql" have been added to the 'open'-type logs. These will contain either "ok" if we successfully logged to the corresponding log database, "no" if it is disabled, or "error $aDetailedMessage" if we got an error trying to insert the row. The 'close'-type log also has the new "accountsql_close" field, but misses the "globalsql_close" field as we never update the global database on this event. On the 'close' log, we can also have the value "missing", indicating that we couldn't update the access log row in the database, as the corresponding 'open' log couldn't insert it. The "ttyrecsize" log field for the 'close'-type logs has been removed, as it was never completely implemented, and contains bogus data if ttyrec log rotation occurs. It has also been removed from the sqlite log databases. The 'open' and 'close' events are now pushed to our own log files, in addition to syslog, if logging to those files is enabled (see ``enableGlobalAccesssLog`` and ``enableAccountAccessLog``), previously the 'close' events were only pushed to syslog. The /home/osh.log is no longer used for ``enableGlobalAccessLog``, the global log is instead written to /home/logkeeper/global-log-YYYYMM.log. The global sql file, enabled with ``enableGlobalSqlLog``, is now split by year-month instead of by year, to /home/logkeeper/global-log-YYYYMM.sqlite.
2020-12-15 23:39:20 +08:00
['action', 'create'],
['account', $account],
['account_uid', $uid],
['public_key', @vettedKeys ? $vettedKeys[0] : undef],
2020-10-16 00:32:37 +08:00
['always_active', ($alwaysActive ? 'true' : 'false')],
['uid_auto', ($uidAuto ? 'true' : 'false')],
['osh_only', ($oshOnly ? 'true' : 'false')],
['immutable_key', ($immutableKey ? 'true' : 'false')],
['comment', $comment],
]
);
HEXIT('OK');