Compare commits

..

1 Commits

Author SHA1 Message Date
Patrick Van der Veken
da092d9a1b fix for sudoers.d ownership on HP-UX (VRF 1.3.4) [Patrick Van der Veken] 2015-09-29 07:51:55 +02:00
6 changed files with 403 additions and 1307 deletions

View File

@ -1,34 +1,18 @@
<p align="center"><img src="logo.png" alt="SUDO Controls Logo"></p>
## What's new
:loudspeaker: **27/04/2025**:
* added the `ignore_errors` flag to allow uninterrupted deployment of fragements.
## About
# SUDO Controls
SUDO Controls is a light-weight **SUDO fragments/rules** distribution & management framework which:
* uses a **desired state** model: SUDO Controls *pushes* fragments from a master (or slave) server onto client host(s) and applies them according to the central configuration.
* uses a **desired state** model: SUDO Controls pushes fragments from a master server onto client host and applies them according to the central configuration.
* uses **SSH** as **transport** mechanism: SUDO Controls connects to client hosts through the secure path of SSH.
* supports a **Master→Slave→Client** model so that information can be propagated within more complex LAN set-ups.
* performs operations with **least privileges**: copy/distribute operations are performed with a low-privileged account. Only the actual snippet updates requires super-user privileges.
* uses a **two-stage** approach to activate **SUDO fragments**: copy (or distribute) and apply. Fragments are first copied into a temporary location on each client hosts - the holding directory - and not applied automatically. Applying or activating fragments on a client host is a separate operation which can be triggered either locally or remotely (from the SUDO master)
* allows the use of (nested) **groups** in the master configuration: fragments and hosts can be grouped in the SUDO master configuration files to allow a simplified configuration. Nesting of groups is allowed up to *5 levels* deep.
* allows the use of (nested) **groups** in the specification of the *push* targets. Either via the `--targets` command-line parameter or via the `targets` configuration file.
* can discover SSH host public keys to (re)create `known_hosts` file(s) for a large amount of hosts
* allows the use of (nested) **groups** in the master configuration: users, fragments and hosts can be grouped in the SUDO master configuration files to allow a simplified configuration. Nesting of groups is allowed up to one level deep.
* requires **no client agent** component and is **stateless**: SUDO Controls performs operations by pushing fragments or commands to client hosts. Update processes on the client hosts will only be started on-demand. If the SUDO master is - for whatever reason - unavailable then active fragments on a client host remain in place.
* is **easy** to **configure** and **maintain** (command-line based): the configuration is stored in a limited number of flat files and be easily updated. A very rudimentary syntax checking facility is also available to check the consistency of the most important (master) configuration files.
More documentation can be found at <https://www.kudos.be/sudo_controls/>
*Logo created with [Free Logo Maker](https://logomakr.com)*
More documentation can be found at http://www.kudos.be/Projects/SUDO_Controls.html

BIN
logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

View File

@ -13,7 +13,7 @@
# (leave blank for current user)
SUDO_TRANSFER_USER=""
# name of the UNIX group that should own the SUDO controls files (must exist already)
# name of the OS group that should own the SUDO controls files
SUDO_OWNER_GROUP="sudoadmin"
# whether a 'chmod' needs to be executed after each sftp transfer [0=No; 1=Yes]
@ -32,8 +32,7 @@ LOCAL_DIR="/etc/sudo_master"
REMOTE_DIR="/etc/sudo_controls/holding"
# name of the user account performing the SUDO controls update
# (leave blank for current user running script)
# user should have remote sudo root privs (except when using user 'root')
# (leave blank for current user but user should have remote sudo root privs)
SUDO_UPDATE_USER=""
# options to pass to update_sudo.pl when executing a key update
@ -42,20 +41,6 @@ SUDO_UPDATE_OPTS="--verbose"
# path to the visudo tool
VISUDO_BIN="/usr/sbin/visudo"
# path to the ssh-keyscan too
SSH_KEYSCAN_BIN="/usr/bin/ssh-keyscan"
# extra arguments/options for the ssh-keyscan command
# by default -f <file> is used by manage_sudo.sh to supply hostnames, do not add here
SSH_KEYSCAN_ARGS="-t rsa"
# whether to start an SSH agent process for the master->client operations [0=No; 1=Yes]
DO_SSH_AGENT=0
# location of the SSH private key that should be added to the SSH agent process
# must be a passphrase-less key (required when using DO_SSH_AGENT)
SSH_PRIVATE_KEY="$HOME/.ssh/id_rsa"
# maximum number of background process to spawn (~maxuprc, ~nstrpty etc)
MAX_BACKGROUND_PROCS=30

File diff suppressed because it is too large Load Diff

View File

@ -12,9 +12,6 @@
# use short hostnames or FQDN (0=short names; 1=FQDN) [default: 0]
use_fqdn=1
# ignore errors during fragment deployment (0=no; 1=yes [default: 0])
ignore_errors=0
# target directory for sudo fragment files
fragments_dir=/etc/sudo_controls/sudoers.d

View File

@ -2,7 +2,7 @@
#******************************************************************************
# @(#) update_sudo.pl
#******************************************************************************
# @(#) Copyright (C) 2014 by KUDOS BV <info@kudos.be>. All rights reserved.
# @(#) Copyright (C) 2014 by KUDOS BVBA <info@kudos.be>. All rights reserved.
#
# This program is a free software; you can redistribute it and/or modify
# it under the same terms of the GNU General Public License as published by
@ -43,23 +43,21 @@ use File::Temp qw(tempfile);
#******************************************************************************
# ------------------------- CONFIGURATION starts here -------------------------
# define the version (YYYY-MM-DD)
my $script_version = "2025-04-27";
# define the V.R.F (version/release/fix)
my $MY_VRF = "1.1.4";
# name of global configuration file (no path, must be located in the script directory)
my $global_config_file = "update_sudo.conf";
# name of localized configuration file (no path, must be located in the script directory)
my $local_config_file = "update_sudo.conf.local";
# maxiumum level of recursion for alias resolution
my $max_recursion = 5;
# selinux context label of sudoers fragment files
my $selinux_context = "etc_t";
# ------------------------- CONFIGURATION ends here ---------------------------
# initialize variables
my ($debug, $verbose, $preview, $global, $use_fqdn, $ignore_errors) = (0,0,0,0,0,0);
my ($debug, $verbose, $preview, $global, $use_fqdn) = (0,0,0,0,0);
my (@config_files, $fragments_dir, $visudo_bin, $immutable_self_file, $immutable_self_cmd);
my (%options, @uname, %aliases, %frags, @grants);
my ($os, $host, $hostname, $run_dir);
my ($selinux_status, $has_selinux, $recursion_count) = ("",0,1);
my ($selinux_status, $has_selinux) = ("",0);
$|++;
@ -89,7 +87,7 @@ sub parse_config_file {
my $config_file = shift;
unless (open (CONF_FD, "<", $config_file)) {
do_log ("ERROR: failed to open the configuration file ${config_file} [$!/$hostname]")
do_log ("ERROR: failed to open the configuration file ${config_file} [$! $hostname]")
and exit (1);
}
while (<CONF_FD>) {
@ -98,14 +96,10 @@ sub parse_config_file {
if (/^\s*$/ || /^#/) {
next;
} else {
if (/^\s*use_fqdn\s*=\s*(0|1)\s*$/) {
if (/^\s*use_fqdn\s*=\s*([0-9]+)\s*$/) {
$use_fqdn = $1;
do_log ("DEBUG: picking up setting: use_fqdn=${use_fqdn}");
}
if (/^\s*ignore_errors\s*=\s*(0|1)\s*$/) {
$ignore_errors = $1;
do_log ("DEBUG: picking up setting: ignore_errors=${ignore_errors}");
}
if (/^\s*fragments_dir\s*=\s*([0-9A-Za-z_\-\.\/~]+)\s*$/) {
$fragments_dir = $1;
do_log ("DEBUG: picking up setting: fragments_dir=${fragments_dir}");
@ -160,24 +154,12 @@ sub set_file {
my ($file, $perm, $uid, $gid) = @_;
my $rc = chmod ($perm, "$file");
if (!$rc) {
if ($ignore_errors) {
do_log ("ERROR: cannot set permissions on $file [$!/$hostname] -- IGNORED");
} else {
do_log ("ERROR: cannot set permissions on $file [$!/$hostname]");
exit (1);
}
}
my $rc = chown ($uid, $gid, "$file");
if (!$rc) {
if ($ignore_errors) {
do_log ("ERROR: cannot set ownerships on $file [$!/$hostname] -- IGNORED");
} else {
do_log ("ERROR: cannot set ownerships on $file [$!/$hostname]");
exit (1);
}
}
chmod ($perm, "$file")
or do_log ("ERROR: cannot set permissions on $file [$! $hostname]")
and exit (1);
chown ($uid, $gid, "$file")
or do_log ("ERROR: cannot set ownerships on $file [$! $hostname]")
and exit (1);
return (1);
}
@ -198,7 +180,6 @@ if ( @ARGV > 0 ) {
debug|d
help|h|?
global|g
ignore|i
preview|p
verbose|v
version|V
@ -209,7 +190,7 @@ pod2usage(-verbose => 0) unless (%options);
# check version parameter
if ($options{'version'}) {
$verbose = 1;
do_log ("INFO: $0: version $script_version");
do_log ("INFO: $0: version $MY_VRF");
exit (0);
}
# check help parameter
@ -221,10 +202,6 @@ if ($options{'help'}) {
if ($options{'global'}) {
$global = 1;
}
# check ignore parameter
if ($options{'ignore'}) {
$ignore_errors = 1;
}
# check preview parameter
if ($options{'preview'}) {
$preview = 1;
@ -250,7 +227,7 @@ $verbose = 1 if ($options{'verbose'});
# where am I? (1/2)
$0 =~ /^(.+[\\\/])[^\\\/]+[\\\/]*$/;
$run_dir = $1 || ".";
my $run_dir = $1 || ".";
$run_dir =~ s#/$##; # remove trailing slash
# don't do anything without configuration file(s)
@ -313,7 +290,7 @@ do_log ("INFO: runtime info: ".getpwuid ($<)."; ${hostname}\@${run_dir}; Perl v$
do_log ("INFO: reading 'alias' file ...");
open (ALIASES, "<", "${run_dir}/alias")
or do_log ("ERROR: cannot read 'alias' file [$!/$hostname]") and exit (1);
or do_log ("ERROR: cannot read 'alias' file [$! $hostname]") and exit (1);
while (<ALIASES>) {
my ($key, $value, @values);
@ -330,44 +307,13 @@ close (ALIASES);
do_log ("DEBUG: dumping unexpanded aliases:");
print Dumper (\%aliases) if $debug;
# resolve aliases recursively to a maxium of $max_recursion
while ($recursion_count <= $max_recursion) {
# crawl over all items in the hash %aliases
# we can nest aliases one level deep, so do a one-level recursive sort of lookup
# of the remaining '@' aliases. Input should be passed as comma-separated
# string to resolve_aliases so don't forget to smash everything back together
# first.
foreach my $key (keys (%aliases)) {
# crawl over all items in the array @{aliases{$key}}
my @new_array; my @filtered_array; # these are the working stashes
do_log ("DEBUG: expanded alias $key before recursion $recursion_count [$hostname]");
print Dumper (\@{$aliases{$key}}) if $debug;
foreach my $item (@{$aliases{$key}}) {
# is it a group?
if ($item =~ /^\@/) {
# expand the group if it exists
if ($aliases{$item}) {
# add current and new items to the working stash
if (@new_array) {
push (@new_array, @{$aliases{$item}});
} else {
@new_array = (@{$aliases{$key}}, @{$aliases{$item}});
}
# remove the original group item from the working stash
@filtered_array = grep { $_ ne $item } @new_array;
@new_array = @filtered_array;
} else {
do_log ("WARN: unable to resolve alias $item [$hostname]");
}
# no group, just add the item as-is to working stash
} else {
push (@new_array, $item);
}
}
my %seen;
@filtered_array = grep { not $seen{$_}++ } @new_array;
# re-assign working stash back to our original hash key
@{$aliases{$key}} = @filtered_array;
do_log ("DEBUG: expanded alias $key after recursion $recursion_count [$hostname]");
print Dumper (\@{$aliases{$key}}) if $debug;
}
$recursion_count++;
$aliases{$key} = [resolve_aliases (join (",", @{$aliases{$key}}))];
}
do_log ("INFO: ".scalar (keys (%aliases))." aliases found on $hostname");
@ -391,7 +337,7 @@ if (-d "${run_dir}/fragments.d" && -f "${run_dir}/fragments") {
if (-d "${run_dir}/fragments.d") {
do_log ("INFO: local 'fragments' are stored in a DIRECTORY on $hostname");
opendir (FRAGS_DIR, "${run_dir}/fragments.d")
or do_log ("ERROR: cannot open 'fragments.d' directory [$!/$hostname]")
or do_log ("ERROR: cannot open 'fragments.d' directory [$! $hostname]")
and exit (1);
while (my $frag_file = readdir (FRAGS_DIR)) {
next if ($frag_file =~ /^\./);
@ -409,7 +355,7 @@ if (-d "${run_dir}/fragments.d") {
# process 'fragments' files
foreach my $frag_file (@frag_files) {
open (FRAGS, "<", $frag_file)
or do_log ("ERROR: cannot read 'fragments' file [$!/$hostname]")
or do_log ("ERROR: cannot read 'fragments' file [$! $hostname]")
and exit (1);
do_log ("INFO: reading SUDO fragments from file: $frag_file");
@ -466,7 +412,7 @@ foreach my $frag_file (@frag_files) {
# strip off path from file name for hash key
$frag_file = fileparse ($frag_file, qr/\.[^.]*/);
do_log ("INFO: fragment file $frag_file contains only 1 fragment on $hostname");
$frags{$frag_file} = join ("\n", @frag_file);
$frags{$frag_file} = join (/\n/, @frag_file);
}
close (FRAGS);
}
@ -503,7 +449,7 @@ if ($? == 0) {
do_log ("INFO: reading 'grants' file ...");
open (GRANTS, "<", "${run_dir}/grants")
or do_log ("ERROR: cannot read 'grants' file [$!/$hostname]") and exit (1);
or do_log ("ERROR: cannot read 'grants' file [$! $hostname]") and exit (1);
while (<GRANTS>) {
my ($what, $where, @what, @where);
@ -544,7 +490,7 @@ print Dumper(\@grants) if $debug;
if ($preview && $global) {
open (GRANTS, "<", "${run_dir}/grants")
or do_log ("ERROR: cannot read 'grants' file [$!/$hostname]") and exit (1);
or do_log ("ERROR: cannot read 'grants' file [$! $hostname]") and exit (1);
while (<GRANTS>) {
my ($what, $where, @what, @where);
@ -595,7 +541,7 @@ unless ($preview) {
# remove previous fragment files first
opendir (FRAGS_DIR, "${fragments_dir}")
or do_log ("ERROR: cannot open ${fragments_dir} directory [$!/$hostname]")
or do_log ("ERROR: cannot open ${fragments_dir} directory [$! $hostname]")
and exit (1);
while (my $frag_file = readdir (FRAGS_DIR)) {
next if ($frag_file =~ /^\./ or $frag_file eq $immutable_self_file);
@ -608,7 +554,7 @@ while (my $frag_file = readdir (FRAGS_DIR)) {
if (unlink ($frag_file)) {
do_log ("INFO: de-activating fragment file $frag_file on $hostname");
} else {
do_log ("ERROR: cannot de-activate fragment file(s) [$!/$hostname]");
do_log ("ERROR: cannot de-activate fragment file(s) [$! $hostname]");
exit (1);
}
}
@ -625,7 +571,7 @@ foreach my $grant (@grants) {
unless ($preview) {
open (SUDO_FILE, "+>", $sudo_file)
or do_log ("ERROR: cannot open file for writing in $fragments_dir [$!/$hostname]")
or do_log ("ERROR: cannot open file for writing in $fragments_dir [$! $hostname]")
and exit (1);
}
print SUDO_FILE "$frags{$grant}\n" unless $preview;
@ -660,7 +606,7 @@ unless ($preview) {
my $self_file = "$fragments_dir/$immutable_self_file";
open (SELF_FILE, "+>", $self_file)
or do_log ("ERROR: cannot open file for writing in $fragments_dir [$!/$hostname]")
or do_log ("ERROR: cannot open file for writing in $fragments_dir [$! $hostname]")
and exit (1);
print SELF_FILE "# THIS IS THE IMMUTABLE SELF FRAGMENT OF SUDO CONTROLS\n";
@ -703,7 +649,6 @@ update_sudo.pl - distributes SUDO fragments according to a desired state model.
update_sudo.pl [-d|--debug]
[-h|--help]
[-i|--ignore]
([-p|--preview] [-g|--global])
[-v|--verbose]
[-V|--version]
@ -714,7 +659,7 @@ update_sudo.pl - distributes SUDO fragments according to a desired state model.
B<update_sudo.pl> distributes SUDO fragments into the C<$fragments_dir> repository based on the F<grants>, F<alias> and F<fragments> files.
This script should be run on each host where SUDO is the required method of privilege escalation.
Orginally SUDO fragments must be stored in a generic F<fragments> file within the same directory as B<update_sudo.pl> script.
For update SUDO fragments must be stored in a generic F<fragments> file within the same directory as B<update_sudo.pl> script.
Alternatively SUDO fragments may be stored as set of individual files within a called sub-directory called F<fragments.d>.
Both methods are mutually exclusive and the latter always take precedence.
@ -738,8 +683,6 @@ Following settings must be configured:
=item * B<use_fqdn> : whether to use short or FQDN host names
=item * B<ignore_errors> : whether to ignore errors during fragment deployment
=item * B<fragments_dir> : target directory for SUDO fragments files
=item * B<visudo_bin> : path to the visudo tool (for sudo rules syntax checking)
@ -760,10 +703,6 @@ S< >Be I<very> verbose during execution; show array/hash dumps.
S< >Show the help page.
=item -i | --ignore
S< >Ignore errors during fragment deployment.
=item -p | --preview
S< >Do not actually distribute any SUDO fragments, nor update/remove SUDO files.
@ -794,4 +733,16 @@ S< >Show version of the script.
=head1 AUTHOR
(c) KUDOS BV, Patrick Van der Veken
(c) KUDOS BVBA, Patrick Van der Veken
=head1 HISTORY
@(#) 2014-12-04: VRF 1.0.0: first version [Patrick Van der Veken]
@(#) 2014-12-16: VRF 1.0.1: added SELinux context [Patrick Van der Veken]
@(#) 2014-12-16: VRF 1.0.2: fixed a problem with the immutable self fragment code [Patrick Van der Veken]
@(#) 2015-02-02: VRF 1.0.3: changed 'basename' into 'fileparse' call to support fragment files with extensions [Patrick Van der Veken]
@(#) 2015-08-18: VRF 1.1.0: replace uname/hostname syscalls, now support for FQDN via $use_fqdn, other fixes [Patrick Van der Veken]
@(#) 2015-08-26: VRF 1.1.1: small and not so small fixes [Patrick Van der Veken]
@(#) 2015-08-27: VRF 1.1.2: small fix [Patrick Van der Veken]
@(#) 2015-09-09: VRF 1.1.3: small selinux fix [Patrick Van der Veken]
@(#) 2015-09-09: VRF 1.1.4: wrong handling of RC=0 in system() [Patrick Van der Veken]