Compare commits
47 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
99bcd4b590 | ||
|
4d7a988618 | ||
64bb1b31d7 | |||
8f12a0b976 | |||
b484d9a264 | |||
b8b5851705 | |||
|
520db1e36d | ||
|
ed4f078b9a | ||
71eb0acc02 | |||
|
c8297aaad2 | ||
|
902dd31116 | ||
|
d90ad7d30b | ||
f1bcf88a7d | |||
|
dfb8da92c8 | ||
|
ec158b2c62 | ||
|
c3fdf58666 | ||
|
fe836edadf | ||
|
7c9f951f62 | ||
|
a311d00924 | ||
|
77a332e324 | ||
|
b8004afe62 | ||
|
9e6ca1b813 | ||
|
de2065b20a | ||
|
5cee831a59 | ||
|
ff9f1b3f93 | ||
|
a99decbf95 | ||
|
af8bf6a665 | ||
|
65e336d1b1 | ||
|
d59f91997a | ||
|
f18086b0e2 | ||
|
69ef9a72e0 | ||
|
ad9f5782a4 | ||
|
49229f9d55 | ||
|
b1db0c5a9b | ||
|
f03a0403ca | ||
|
c377ee9a7a | ||
|
7f23f5ac8b | ||
|
da24312c22 | ||
|
9aa4d8cbd3 | ||
|
7ca0a08be7 | ||
|
871e7ca1bb | ||
|
7d337f3185 | ||
|
34f358f7fb | ||
|
19cccb4a4c | ||
|
be909a8f5b | ||
|
bc952e64c2 | ||
|
0db8e3c51d |
30
README.md
30
README.md
@ -1,11 +1,21 @@
|
|||||||
# SSH Controls
|
<p align="center"><img src="logo.png" alt="SSH Controls Logo"></p>
|
||||||
|
|
||||||
|
## What's new
|
||||||
|
|
||||||
|
:loudspeaker: **25/07/2025**:
|
||||||
|
* added the creation of the `$HOME/.ssh` directory when running as `root`.
|
||||||
|
|
||||||
|
## About
|
||||||
|
|
||||||
SSH Controls is a light-weight SSH **public key** distribution & management framework
|
SSH Controls is a light-weight SSH **public key** distribution & management framework
|
||||||
|
|
||||||
* uses a **desired state** model: SSH Controls pushes public keys from a key master server onto client host and applies them according to the central configuration.
|
* uses a **desired state** model: SSH Controls *pushes* public keys from a key master (or slave) server onto client host(s) and applies them according to the central configuration.
|
||||||
|
|
||||||
* uses **SSH** as **transport** mechanism: eat your own dogfood. SSH Controls connects to client hosts through the secure path of SSH and using a public key that is under its own control.
|
* uses **SSH** as **transport** mechanism: eat your own dog food. SSH Controls connects to client hosts through the secure path of SSH and using a public key that is under its own control.
|
||||||
|
|
||||||
* **shields** public keys from owners/users on client systems: SSH Controls requires the standard sshd_config to be reconfigured with an alternate path for the AuthorizedKeysFile setting so that public keys are stored in common location which cannot be manipulated by the owners of the public keys. This allows for more administrative control and better security.
|
* supports a **Master→Slave→Client** model so that information can be propagated within more complex LAN set-ups.
|
||||||
|
|
||||||
|
* can **shield** public keys from owners/users on client systems: SSH Controls may require the standard `sshd_config` to be reconfigured with an alternate path for the `AuthorizedKeysFile` setting so that public keys are stored in common location which cannot be manipulated by the owners of the public keys. This allows for more administrative control and better security.
|
||||||
|
|
||||||
* performs operations with **least privileges**: copy/distribute operations are performed with a low-privileged account. Only the actual key updates requires super-user privileges which need to be configured via SUDO.
|
* performs operations with **least privileges**: copy/distribute operations are performed with a low-privileged account. Only the actual key updates requires super-user privileges which need to be configured via SUDO.
|
||||||
|
|
||||||
@ -13,10 +23,16 @@ SSH Controls is a light-weight SSH **public key** distribution & management fram
|
|||||||
|
|
||||||
* can assign a single public key to **multiple** OS accounts: SSH Controls allows an user to log on under multiple accounts using the same key. Auditing of the connecting user and the target account is possible using fingerprinting.
|
* can assign a single public key to **multiple** OS accounts: SSH Controls allows an user to log on under multiple accounts using the same key. Auditing of the connecting user and the target account is possible using fingerprinting.
|
||||||
|
|
||||||
* allows the use of (nested) **groups** in the master configuration: users, keys and hosts can be grouped in the SSH master configuration files to allow a simplified configuration. Nesting of groups is allowed up to one level deep.
|
* allows the use of (nested) **groups** in the master configuration: users, keys and hosts can be grouped in the SSH 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.
|
||||||
|
|
||||||
* allows compromised public keys to be **blacklisted**: SSH Controls will deny the use of public keys that have been administrative blacklisted. Blacklisting happens on the SSH master and is applied to all client hosts.
|
* allows compromised public keys to be **blacklisted**: SSH Controls will deny the use of public keys that have been administrative blacklisted. Blacklisting happens on the SSH master and is applied to all client hosts.
|
||||||
|
|
||||||
|
* can discover host public keys to (re)create `known_hosts` file(s) for a large amount of hosts
|
||||||
|
|
||||||
|
* supports *md5* and *sha512* fingerprint **hashes** (if the installed SSH version supports these hash types)
|
||||||
|
|
||||||
* requires **no client agent** component and is **stateless**: SSH Controls performs operations by pushing keys or commands to client hosts. Update processes on the client hosts will only be started on-demand. If the SSH master is - for whatever reason - unavailable then active keys on a client host remain in place and logons are still possible.
|
* requires **no client agent** component and is **stateless**: SSH Controls performs operations by pushing keys or commands to client hosts. Update processes on the client hosts will only be started on-demand. If the SSH master is - for whatever reason - unavailable then active keys on a client host remain in place and logons are still possible.
|
||||||
|
|
||||||
* 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.
|
* 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.
|
||||||
@ -26,6 +42,6 @@ SSH Controls does NOT:
|
|||||||
|
|
||||||
* manage or distribute SSH **private keys**: SSH private keys should be controlled and managed (and safeguarded!) by the actual owners. Though one could consider SSH key pairs of generic accounts (such as application accounts) as an exception, SSH Controls currently does not support the management of private keys.
|
* manage or distribute SSH **private keys**: SSH private keys should be controlled and managed (and safeguarded!) by the actual owners. Though one could consider SSH key pairs of generic accounts (such as application accounts) as an exception, SSH Controls currently does not support the management of private keys.
|
||||||
|
|
||||||
* discover SSH **host keys**: SSH Controls will silently ignore any questions related to host keys discoveries upon the first connection to client hosts. If you are managing a large number of client hosts, you may want to prepare a known_hosts file in advance or else let the host keys be added automatically upon first public key distribution.
|
More documentation can be found at <https://www.kudos.be/ssh_controls/>
|
||||||
|
|
||||||
More documentation can be found at http://www.kudos.be/Projects/SSH_Controls.html
|
*Logo created with [Free Logo Maker](https://logomakr.com)*
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
#******************************************************************************
|
#******************************************************************************
|
||||||
# @(#) convert_pubkey.pl
|
# @(#) convert_pubkey.pl
|
||||||
#******************************************************************************
|
#******************************************************************************
|
||||||
# @(#) Copyright (C) 2014 by KUDOS BVBA <info@kudos.be>. All rights reserved.
|
# @(#) Copyright (C) 2014 by KUDOS BV <info@kudos.be>. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is a free software; you can redistribute it and/or modify
|
# 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
|
# it under the same terms of the GNU General Public License as published by
|
||||||
@ -29,7 +29,7 @@ use Pod::Usage;
|
|||||||
|
|
||||||
# ------------------------- CONFIGURATION starts here -------------------------
|
# ------------------------- CONFIGURATION starts here -------------------------
|
||||||
# define the V.R.F (version/release/fix)
|
# define the V.R.F (version/release/fix)
|
||||||
my $MY_VRF = "1.0.0";
|
my $script_version = "2025-04-27";
|
||||||
# always assume RSA keys!
|
# always assume RSA keys!
|
||||||
my $key_algo = 'ssh-rsa';
|
my $key_algo = 'ssh-rsa';
|
||||||
# ------------------------- CONFIGURATION ends here ---------------------------
|
# ------------------------- CONFIGURATION ends here ---------------------------
|
||||||
@ -62,7 +62,7 @@ pod2usage(-verbose => 0) unless (%options);
|
|||||||
|
|
||||||
# check version parameter
|
# check version parameter
|
||||||
if ($options{'version'}) {
|
if ($options{'version'}) {
|
||||||
print "INFO: $0: version $MY_VRF";
|
print "INFO: $0: version $script_version";
|
||||||
exit (0);
|
exit (0);
|
||||||
}
|
}
|
||||||
# check help parameter
|
# check help parameter
|
||||||
@ -194,8 +194,4 @@ S< >Show version of the script.
|
|||||||
|
|
||||||
=head1 AUTHOR
|
=head1 AUTHOR
|
||||||
|
|
||||||
(c) KUDOS BVBA, Patrick Van der Veken
|
(c) KUDOS BV, Patrick Van der Veken
|
||||||
|
|
||||||
=head1 HISTORY
|
|
||||||
|
|
||||||
@(#) 2014-12-20: VRF 1.0.0: first version [Patrick Van der Veken]
|
|
@ -13,17 +13,17 @@
|
|||||||
# (leave blank for current user)
|
# (leave blank for current user)
|
||||||
SSH_TRANSFER_USER=""
|
SSH_TRANSFER_USER=""
|
||||||
|
|
||||||
# name of the OS group that should own the SSH controls files
|
# name of the UNIX group that should own the SSH controls files (must exist already)
|
||||||
SSH_OWNER_GROUP="sshadmin"
|
SSH_OWNER_GROUP="sshadmin"
|
||||||
|
|
||||||
# whether a 'chmod' needs to be executed after each sftp transfer [0=Yes; 1=No]
|
# whether a 'chmod' needs to be executed after each sftp transfer [0=No; 1=Yes]
|
||||||
DO_SFTP_CHMOD=0
|
DO_SFTP_CHMOD=1
|
||||||
|
|
||||||
# extra arguments/options for the SFTP command
|
# extra arguments/options for the SFTP command
|
||||||
SFTP_ARGS="-o StrictHostKeyChecking=no -o ConnectTimeout=10 -b - "
|
SFTP_ARGS="-o StrictHostKeyChecking=no -o ConnectTimeout=10 -o BatchMode=yes -b - "
|
||||||
|
|
||||||
# extra arguments/options for the SSH command
|
# extra arguments/options for the SSH command
|
||||||
SSH_ARGS="-o StrictHostKeyChecking=no -o ConnectTimeout=10 -n"
|
SSH_ARGS="-o StrictHostKeyChecking=no -o ConnectTimeout=10 -o BatchMode=yes -n"
|
||||||
|
|
||||||
# location of the local SSH controls directory
|
# location of the local SSH controls directory
|
||||||
LOCAL_DIR="/etc/ssh_master"
|
LOCAL_DIR="/etc/ssh_master"
|
||||||
@ -32,12 +32,27 @@ LOCAL_DIR="/etc/ssh_master"
|
|||||||
REMOTE_DIR="/etc/ssh_controls/holding"
|
REMOTE_DIR="/etc/ssh_controls/holding"
|
||||||
|
|
||||||
# name of the user account performing the SSH controls update
|
# name of the user account performing the SSH controls update
|
||||||
# (leave blank for current user but user should have remote sudo root privs)
|
# (leave blank for current user running script)
|
||||||
|
# user should have remote sudo root privs (except when using user 'root')
|
||||||
SSH_UPDATE_USER=""
|
SSH_UPDATE_USER=""
|
||||||
|
|
||||||
# options to pass to update_ssh.pl when executing a key update
|
# options to pass to update_ssh.pl when executing a key update
|
||||||
SSH_UPDATE_OPTS="--verbose --remove"
|
SSH_UPDATE_OPTS="--verbose --remove"
|
||||||
|
|
||||||
|
# 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)
|
# maximum number of background process to spawn (~maxuprc, ~nstrpty etc)
|
||||||
MAX_BACKGROUND_PROCS=30
|
MAX_BACKGROUND_PROCS=30
|
||||||
|
|
||||||
@ -47,6 +62,10 @@ BACKUP_DIR="${LOCAL_DIR}/backup"
|
|||||||
# location of log directory (default), see --log-dir)
|
# location of log directory (default), see --log-dir)
|
||||||
LOG_DIR="/var/log"
|
LOG_DIR="/var/log"
|
||||||
|
|
||||||
|
# type of fingerpint (md5, sha256)
|
||||||
|
FINGERPRINT_TYPE="md5"
|
||||||
|
|
||||||
|
|
||||||
#******************************************************************************
|
#******************************************************************************
|
||||||
# End of FILE
|
# End of FILE
|
||||||
#******************************************************************************
|
#******************************************************************************
|
||||||
|
1808
manage_ssh.sh
1808
manage_ssh.sh
File diff suppressed because it is too large
Load Diff
@ -12,12 +12,23 @@
|
|||||||
# use short hostnames or FQDN (0=short names; 1=FQDN) [default: 0]
|
# use short hostnames or FQDN (0=short names; 1=FQDN) [default: 0]
|
||||||
use_fqdn=1
|
use_fqdn=1
|
||||||
|
|
||||||
|
# ignore errors during key deployment (0=no; 1=yes [default: 0])
|
||||||
|
ignore_errors=0
|
||||||
|
|
||||||
# target directory for allowed SSH key files
|
# target directory for allowed SSH key files
|
||||||
access_dir=/etc/kudos/ssh_controls/keys.d
|
access_dir=/etc/ssh_controls/keys.d
|
||||||
|
|
||||||
|
# toggle to specify the final location of public keys by allowing to override
|
||||||
|
# the value of $access_dir with the 'AuthorizedKeysFile' in sshd (=enables the
|
||||||
|
# use of $HOME/.ssh for public keys for example):
|
||||||
|
# 'use_controls': take the value from the configured 'access_dir' option
|
||||||
|
# 'use_sshd' : use the value from 'AuthorizedKeysFile' setting in sshd
|
||||||
|
# [default: use_controls]
|
||||||
|
key_location=use_controls
|
||||||
|
|
||||||
# location of the keys blacklist file
|
# location of the keys blacklist file
|
||||||
blacklist_file=/etc/kudos/ssh_controls/keys.blacklisted
|
blacklist_file=/etc/ssh_controls/keys.blacklisted
|
||||||
|
|
||||||
#******************************************************************************
|
#******************************************************************************
|
||||||
# End of FILE
|
# End of FILE
|
||||||
#******************************************************************************
|
#******************************************************************************
|
||||||
|
459
update_ssh.pl
459
update_ssh.pl
@ -2,7 +2,7 @@
|
|||||||
#******************************************************************************
|
#******************************************************************************
|
||||||
# @(#) update_ssh.pl
|
# @(#) update_ssh.pl
|
||||||
#******************************************************************************
|
#******************************************************************************
|
||||||
# @(#) Copyright (C) 2014 by KUDOS BVBA <info@kudos.be>. All rights reserved.
|
# @(#) Copyright (C) 2014 by KUDOS BV <info@kudos.be>. All rights reserved.
|
||||||
#
|
#
|
||||||
# This program is a free software; you can redistribute it and/or modify
|
# 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
|
# it under the same terms of the GNU General Public License as published by
|
||||||
@ -41,23 +41,34 @@ use Pod::Usage;
|
|||||||
#******************************************************************************
|
#******************************************************************************
|
||||||
|
|
||||||
# ------------------------- CONFIGURATION starts here -------------------------
|
# ------------------------- CONFIGURATION starts here -------------------------
|
||||||
# define the V.R.F (version/release/fix)
|
# define the version (YYYY-MM-DD)
|
||||||
my $MY_VRF = "1.2.0";
|
my $script_version = "2025-07-25";
|
||||||
# name of global configuration file (no path, must be located in the script directory)
|
# name of global configuration file (no path, must be located in the script directory)
|
||||||
my $global_config_file = "update_ssh.conf";
|
my $global_config_file = "update_ssh.conf";
|
||||||
# name of localized configuration file (no path, must be located in the script directory)
|
# name of localized configuration file (no path, must be located in the script directory)
|
||||||
my $local_config_file = "update_ssh.conf.local";
|
my $local_config_file = "update_ssh.conf.local";
|
||||||
|
# maxiumum level of recursion for alias resolution
|
||||||
|
my $max_recursion = 5;
|
||||||
# selinux context labels of key files for different RHEL version
|
# selinux context labels of key files for different RHEL version
|
||||||
my %selinux_contexts = ( '5' => 'sshd_key_t',
|
my %selinux_contexts = ( '5' => 'sshd_key_t',
|
||||||
'6' => 'ssh_home_t',
|
'6' => 'ssh_home_t',
|
||||||
'7' => 'ssh_home_t');
|
'7' => 'ssh_home_t',
|
||||||
# ------------------------- CONFIGURATION ends here ---------------------------
|
'8' => 'ssh_home_t',
|
||||||
|
'9' => 'ssh_home_t',
|
||||||
|
'10' => 'ssh_home_t');
|
||||||
|
# disallowed paths for home directories for accounts
|
||||||
|
my @disallowed_homes = ('/', '/etc', '/bin', '/sbin', '/usr/bin', '/usr/sbin');
|
||||||
|
# disallowed login shells for @accounts
|
||||||
|
my @disallowed_shells = ('/bin/nologin','/bin/false','/sbin/nologin','/sbin/false');
|
||||||
|
# default toggle for key location
|
||||||
|
my $key_location='use_controls';
|
||||||
|
# ------------------------- CONFIGURATION ends here ---------------------------
|
||||||
# initialize variables
|
# initialize variables
|
||||||
my ($debug, $verbose, $preview, $remove, $global, $use_fqdn) = (0,0,0,0,0,0);
|
my ($debug, $verbose, $preview, $remove, $global, $use_fqdn, $ignore_errors) = (0,0,0,0,0,0,0);
|
||||||
my (@config_files, @zombie_files, $access_dir, $blacklist_file);
|
my (@config_files, @zombie_files, $access_dir, $blacklist_file);
|
||||||
my (%options, @uname, @pwgetent, @accounts, %aliases, %keys, %access, @blacklist);
|
my (%options, @uname, @pwgetent, @accounts, %aliases, %keys, %access, @blacklist);
|
||||||
my ($os, $hostname, $run_dir);
|
my ($os, $hostname, $run_dir, $authorizedkeys_option);
|
||||||
my ($selinux_status, $selinux_context, $linux_version, $has_selinux) = ("","","",0);
|
my ($selinux_status, $selinux_context, $linux_version, $has_selinux, $recursion_count) = ("","","",0,1);
|
||||||
$|++;
|
$|++;
|
||||||
|
|
||||||
|
|
||||||
@ -67,7 +78,7 @@ $|++;
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
sub do_log {
|
sub do_log {
|
||||||
|
|
||||||
my $message = shift;
|
my $message = shift;
|
||||||
|
|
||||||
if ($message =~ /^ERROR:/ || $message =~ /^WARN:/) {
|
if ($message =~ /^ERROR:/ || $message =~ /^WARN:/) {
|
||||||
@ -87,7 +98,7 @@ sub parse_config_file {
|
|||||||
my $config_file = shift;
|
my $config_file = shift;
|
||||||
|
|
||||||
unless (open (CONF_FD, "<", $config_file)) {
|
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);
|
and exit (1);
|
||||||
}
|
}
|
||||||
while (<CONF_FD>) {
|
while (<CONF_FD>) {
|
||||||
@ -96,14 +107,25 @@ sub parse_config_file {
|
|||||||
if (/^\s*$/ || /^#/) {
|
if (/^\s*$/ || /^#/) {
|
||||||
next;
|
next;
|
||||||
} else {
|
} else {
|
||||||
if (/^\s*use_fqdn\s*=\s*([0-9]+)\s*$/) {
|
if (/^\s*use_fqdn\s*=\s*(0|1)\s*$/) {
|
||||||
$use_fqdn = $1;
|
$use_fqdn = $1;
|
||||||
do_log ("DEBUG: picking up setting: use_fqdn=${use_fqdn}");
|
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*access_dir\s*=\s*([0-9A-Za-z_\-\.\/~]+)\s*$/) {
|
if (/^\s*access_dir\s*=\s*([0-9A-Za-z_\-\.\/~]+)\s*$/) {
|
||||||
$access_dir = $1;
|
$access_dir = $1;
|
||||||
do_log ("DEBUG: picking up setting: access_dir=${access_dir}");
|
do_log ("DEBUG: picking up setting: access_dir=${access_dir}");
|
||||||
}
|
}
|
||||||
|
if (/^\s*key_location\s*=\s*(use_controls|use_sshd)\s*/) {
|
||||||
|
$key_location = $1;
|
||||||
|
do_log ("DEBUG: picking up setting: key_location=${key_location}");
|
||||||
|
if ($key_location eq 'use_sshd') {
|
||||||
|
do_log ("DEBUG: applied setting: key_location=${key_location}");
|
||||||
|
}
|
||||||
|
}
|
||||||
if (/^\s*blacklist_file\s*=\s*([0-9A-Za-z_\-\.\/~]+)\s*$/) {
|
if (/^\s*blacklist_file\s*=\s*([0-9A-Za-z_\-\.\/~]+)\s*$/) {
|
||||||
$blacklist_file = $1;
|
$blacklist_file = $1;
|
||||||
# support tilde (~) expansion for ~root
|
# support tilde (~) expansion for ~root
|
||||||
@ -118,7 +140,7 @@ sub parse_config_file {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (1);
|
return (1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,7 +154,7 @@ sub resolve_aliases
|
|||||||
foreach $entry (@tmp_array) {
|
foreach $entry (@tmp_array) {
|
||||||
if ($entry =~ /^\@/) {
|
if ($entry =~ /^\@/) {
|
||||||
($aliases{$entry})
|
($aliases{$entry})
|
||||||
? push (@new_array, @{$aliases{$entry}})
|
? push (@new_array, @{$aliases{$entry}})
|
||||||
: do_log ("WARN: unable to resolve alias $entry [$hostname]");
|
: do_log ("WARN: unable to resolve alias $entry [$hostname]");
|
||||||
} else {
|
} else {
|
||||||
($entry)
|
($entry)
|
||||||
@ -147,14 +169,26 @@ sub resolve_aliases
|
|||||||
sub set_file {
|
sub set_file {
|
||||||
|
|
||||||
my ($file, $perm, $uid, $gid) = @_;
|
my ($file, $perm, $uid, $gid) = @_;
|
||||||
|
|
||||||
chmod ($perm, "$file")
|
my $rc = chmod ($perm, "$file");
|
||||||
or do_log ("ERROR: cannot set permissions on $file [$! $hostname]")
|
if (!$rc) {
|
||||||
and exit (1);
|
if ($ignore_errors) {
|
||||||
chown ($uid, $gid, "$file")
|
do_log ("ERROR: cannot set permissions on $file [$!/$hostname] -- IGNORED");
|
||||||
or do_log ("ERROR: cannot set ownerships on $file [$! $hostname]")
|
} else {
|
||||||
and exit (1);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (1);
|
return (1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,18 +208,19 @@ if ( @ARGV > 0 ) {
|
|||||||
debug|d
|
debug|d
|
||||||
help|h|?
|
help|h|?
|
||||||
global|g
|
global|g
|
||||||
|
ignore|i
|
||||||
preview|p
|
preview|p
|
||||||
remove|r
|
remove|r
|
||||||
verbose|v
|
verbose|v
|
||||||
version|V
|
version|V
|
||||||
)) || pod2usage(-verbose => 0);
|
)) || pod2usage(-verbose => 0);
|
||||||
}
|
}
|
||||||
pod2usage(-verbose => 0) unless (%options);
|
pod2usage(-verbose => 0) unless (%options);
|
||||||
|
|
||||||
# check version parameter
|
# check version parameter
|
||||||
if ($options{'version'}) {
|
if ($options{'version'}) {
|
||||||
$verbose = 1;
|
$verbose = 1;
|
||||||
do_log ("INFO: $0: version $MY_VRF");
|
do_log ("INFO: $0: version $script_version");
|
||||||
exit (0);
|
exit (0);
|
||||||
}
|
}
|
||||||
# check help parameter
|
# check help parameter
|
||||||
@ -197,12 +232,16 @@ if ($options{'help'}) {
|
|||||||
if ($options{'global'}) {
|
if ($options{'global'}) {
|
||||||
$global = 1;
|
$global = 1;
|
||||||
}
|
}
|
||||||
|
# check ignore parameter
|
||||||
|
if ($options{'ignore'}) {
|
||||||
|
$ignore_errors = 1;
|
||||||
|
}
|
||||||
# check preview parameter
|
# check preview parameter
|
||||||
if ($options{'preview'}) {
|
if ($options{'preview'}) {
|
||||||
$preview = 1;
|
$preview = 1;
|
||||||
$verbose = 1;
|
$verbose = 1;
|
||||||
if ($global) {
|
if ($global) {
|
||||||
do_log ("INFO: running in GLOBAL PREVIEW mode");
|
do_log ("INFO: running in GLOBAL PREVIEW mode");
|
||||||
} else {
|
} else {
|
||||||
do_log ("INFO: running in PREVIEW mode");
|
do_log ("INFO: running in PREVIEW mode");
|
||||||
}
|
}
|
||||||
@ -226,7 +265,7 @@ $verbose = 1 if ($options{'verbose'});
|
|||||||
|
|
||||||
# where am I? (1/2)
|
# where am I? (1/2)
|
||||||
$0 =~ /^(.+[\\\/])[^\\\/]+[\\\/]*$/;
|
$0 =~ /^(.+[\\\/])[^\\\/]+[\\\/]*$/;
|
||||||
my $run_dir = $1 || ".";
|
$run_dir = $1 || ".";
|
||||||
$run_dir =~ s#/$##; # remove trailing slash
|
$run_dir =~ s#/$##; # remove trailing slash
|
||||||
|
|
||||||
# don't do anything without configuration file(s)
|
# don't do anything without configuration file(s)
|
||||||
@ -234,7 +273,7 @@ do_log ("INFO: parsing configuration file(s) ...");
|
|||||||
push (@config_files, "$run_dir/$global_config_file") if (-f "$run_dir/$global_config_file");
|
push (@config_files, "$run_dir/$global_config_file") if (-f "$run_dir/$global_config_file");
|
||||||
push (@config_files, "$run_dir/$local_config_file") if (-f "$run_dir/$local_config_file");
|
push (@config_files, "$run_dir/$local_config_file") if (-f "$run_dir/$local_config_file");
|
||||||
unless (@config_files) {
|
unless (@config_files) {
|
||||||
do_log ("ERROR: unable to find any configuration file, bailing out [$hostname]")
|
do_log ("ERROR: unable to find any configuration file, bailing out [$hostname]")
|
||||||
and exit (1);
|
and exit (1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -243,30 +282,19 @@ foreach my $config_file (@config_files) {
|
|||||||
parse_config_file ($config_file);
|
parse_config_file ($config_file);
|
||||||
}
|
}
|
||||||
|
|
||||||
# is the target directory for keys present? (not for global preview)
|
# is the target directory for keys present? (not for global preview and
|
||||||
unless ($preview and $global) {
|
# not when $key_location is use_sshd)
|
||||||
do_log ("INFO: checking for SSH control mode ...");
|
unless (($preview and $global) or $key_location eq 'use_sshd') {
|
||||||
|
do_log ("INFO: checking for SSH controls mode ...");
|
||||||
if (-d $access_dir) {
|
if (-d $access_dir) {
|
||||||
do_log ("INFO: host is under SSH control via $access_dir");
|
do_log ("INFO: host is under SSH controls via $access_dir");
|
||||||
} else {
|
} else {
|
||||||
do_log ("ERROR: host is not under SSH keys only control [$hostname]")
|
if ($key_location eq 'use_sshd') {
|
||||||
and exit (1);
|
do_log ("INFO: skipped check since public key location is determined by sshd [$hostname]")
|
||||||
}
|
} else {
|
||||||
}
|
do_log ("ERROR: host is not under SSH keys only control [$hostname]")
|
||||||
|
and exit (1);
|
||||||
# do we have a blacklist file? (optional) (not for global preview)
|
}
|
||||||
unless ($preview and $global) {
|
|
||||||
do_log ("INFO: checking for keys blacklist file ...");
|
|
||||||
if (-f $blacklist_file) {
|
|
||||||
open (BLACKLIST, "<", $blacklist_file) or \
|
|
||||||
do_log ("ERROR: cannot read keys blacklist file [$! $hostname]")
|
|
||||||
and exit (1);
|
|
||||||
@blacklist = <BLACKLIST>;
|
|
||||||
close (BLACKLIST);
|
|
||||||
do_log ("INFO: keys blacklist file found with ".scalar (@blacklist)." entr(y|ies) on $hostname");
|
|
||||||
print Dumper (\@blacklist) if $debug;
|
|
||||||
} else {
|
|
||||||
do_log ("WARN: no keys blacklist file found [$hostname]");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -276,7 +304,7 @@ $os = $uname[0];
|
|||||||
# who am I?
|
# who am I?
|
||||||
unless ($preview and $global) {
|
unless ($preview and $global) {
|
||||||
if ($< != 0) {
|
if ($< != 0) {
|
||||||
do_log ("ERROR: script must be invoked as user 'root' [$hostname]")
|
do_log ("ERROR: script must be invoked as user 'root' [$hostname]")
|
||||||
and exit (1);
|
and exit (1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -289,6 +317,49 @@ if ($use_fqdn) {
|
|||||||
|
|
||||||
do_log ("INFO: runtime info: ".getpwuid ($<)."; ${hostname}\@${run_dir}; Perl v$]");
|
do_log ("INFO: runtime info: ".getpwuid ($<)."; ${hostname}\@${run_dir}; Perl v$]");
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# handle blacklist file
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# do we have a blacklist file? (optional) (not for global preview)
|
||||||
|
unless ($preview and $global) {
|
||||||
|
do_log ("INFO: checking for keys blacklist file ...");
|
||||||
|
if (-f $blacklist_file) {
|
||||||
|
open (BLACKLIST, "<", $blacklist_file) or \
|
||||||
|
do_log ("ERROR: cannot read keys blacklist file [$!/$hostname]")
|
||||||
|
and exit (1);
|
||||||
|
@blacklist = <BLACKLIST>;
|
||||||
|
close (BLACKLIST);
|
||||||
|
do_log ("INFO: keys blacklist file found with ".scalar (@blacklist)." entr(y|ies) on $hostname");
|
||||||
|
print Dumper (\@blacklist) if $debug;
|
||||||
|
} else {
|
||||||
|
do_log ("WARN: no keys blacklist file found [$hostname]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# resolve and check key location
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
if ($key_location eq 'use_sshd') {
|
||||||
|
|
||||||
|
# get sshd setting but only take 1st path into account
|
||||||
|
$authorizedkeys_option = qx#sshd -T | grep "authorizedkeysfile" 2>/dev/null | cut -f2 -d' '#;
|
||||||
|
chomp ($authorizedkeys_option);
|
||||||
|
if (defined ($authorizedkeys_option)) {
|
||||||
|
do_log ("INFO: AuthorizedkeysFile resolves to $authorizedkeys_option [$hostname]");
|
||||||
|
} else {
|
||||||
|
do_log ("ERROR: unable to get AuthorizedkeysFile value from sshd [$hostname]")
|
||||||
|
and exit (1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
# for SSH controls native logic we require an absolute path
|
||||||
|
if ($authorizedkeys_option =~ /^\//) {
|
||||||
|
do_log ("ERROR: option \$access_dir requires and absolute path [$hostname]")
|
||||||
|
and exit (1);
|
||||||
|
}
|
||||||
|
do_log ("DEBUG: applied default setting: key_location=${key_location}");
|
||||||
|
}
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# collect user accounts via getpwent()
|
# collect user accounts via getpwent()
|
||||||
# result: @accounts
|
# result: @accounts
|
||||||
@ -301,7 +372,7 @@ while (@pwgetent = getpwent()) {
|
|||||||
push (@accounts, $pwgetent[0]);
|
push (@accounts, $pwgetent[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
# remove duplicates (which should not happen (!) but local, LDAP and accounts
|
# remove duplicates (which should not happen (!) but local, LDAP and accounts
|
||||||
# from other sources might trample over each other)
|
# from other sources might trample over each other)
|
||||||
my %uniq_accounts = map { $_, 0 } @accounts;
|
my %uniq_accounts = map { $_, 0 } @accounts;
|
||||||
@accounts = keys %uniq_accounts;
|
@accounts = keys %uniq_accounts;
|
||||||
@ -310,18 +381,18 @@ do_log ("INFO: ".scalar (@accounts)." user accounts found on $hostname");
|
|||||||
print Dumper (\@accounts) if $debug;
|
print Dumper (\@accounts) if $debug;
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# read aliases for teams, servers and users
|
# read aliases for teams, servers and users (and resolve group definitions)
|
||||||
# result: %aliases
|
# result: %aliases
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
do_log ("INFO: reading 'alias' file ...");
|
do_log ("INFO: reading 'alias' file ...");
|
||||||
|
|
||||||
open (ALIASES, "<", "${run_dir}/alias")
|
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>) {
|
while (<ALIASES>) {
|
||||||
|
|
||||||
my ($key, $value, @values);
|
my ($key, $value, @values);
|
||||||
|
|
||||||
chomp ();
|
chomp ();
|
||||||
next if (/^$/ || /\#/);
|
next if (/^$/ || /\#/);
|
||||||
s/\s+//g;
|
s/\s+//g;
|
||||||
@ -334,13 +405,45 @@ close (ALIASES);
|
|||||||
do_log ("DEBUG: dumping unexpanded aliases:");
|
do_log ("DEBUG: dumping unexpanded aliases:");
|
||||||
print Dumper (\%aliases) if $debug;
|
print Dumper (\%aliases) if $debug;
|
||||||
|
|
||||||
# we can nest aliases one level deep, so do a one-level recursive sort of lookup
|
# resolve aliases recursively to a maxium of $max_recursion
|
||||||
# of the remaining '@' aliases. Input should be passed as comma-separated
|
while ($recursion_count <= $max_recursion) {
|
||||||
# string to resolve_aliases so don't forget to smash everything back together
|
# crawl over all items in the hash %aliases
|
||||||
# first.
|
foreach my $key (keys (%aliases)) {
|
||||||
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
|
||||||
$aliases{$key} = [resolve_aliases (join (",", @{$aliases{$key}}))];
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# filter out dupes
|
||||||
|
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++;
|
||||||
}
|
}
|
||||||
|
|
||||||
do_log ("INFO: ".scalar (keys (%aliases))." aliases found on $hostname");
|
do_log ("INFO: ".scalar (keys (%aliases))." aliases found on $hostname");
|
||||||
@ -364,25 +467,25 @@ if (-d "${run_dir}/keys.d" && -f "${run_dir}/keys") {
|
|||||||
if (-d "${run_dir}/keys.d") {
|
if (-d "${run_dir}/keys.d") {
|
||||||
do_log ("INFO: local 'keys' are stored in a DIRECTORY on $hostname");
|
do_log ("INFO: local 'keys' are stored in a DIRECTORY on $hostname");
|
||||||
opendir (KEYS_DIR, "${run_dir}/keys.d")
|
opendir (KEYS_DIR, "${run_dir}/keys.d")
|
||||||
or do_log ("ERROR: cannot open 'keys.d' directory [$! $hostname]")
|
or do_log ("ERROR: cannot open 'keys.d' directory [$!/$hostname]")
|
||||||
and exit (1);
|
and exit (1);
|
||||||
while (my $key_file = readdir (KEYS_DIR)) {
|
while (my $key_file = readdir (KEYS_DIR)) {
|
||||||
next if ($key_file =~ /^\./);
|
next if ($key_file =~ /^\./);
|
||||||
push (@key_files, "${run_dir}/keys.d/$key_file");
|
push (@key_files, "${run_dir}/keys.d/$key_file");
|
||||||
}
|
}
|
||||||
closedir (KEYS_DIR);
|
closedir (KEYS_DIR);
|
||||||
} elsif (-f "${run_dir}/keys") {
|
} elsif (-f "${run_dir}/keys") {
|
||||||
do_log ("INFO: local 'keys' are stored in a FILE on $hostname");
|
do_log ("INFO: local 'keys' are stored in a FILE on $hostname");
|
||||||
push (@key_files, "${run_dir}/keys");
|
push (@key_files, "${run_dir}/keys");
|
||||||
} else {
|
} else {
|
||||||
do_log ("ERROR: cannot find any public keys in the repository! [$hostname]")
|
do_log ("ERROR: cannot find any public keys in the repository! [$hostname]")
|
||||||
and exit (1);
|
and exit (1);
|
||||||
}
|
}
|
||||||
|
|
||||||
# process 'keys' files
|
# process 'keys' files
|
||||||
foreach my $key_file (@key_files) {
|
foreach my $key_file (@key_files) {
|
||||||
open (KEYS, "<", $key_file)
|
open (KEYS, "<", $key_file)
|
||||||
or do_log ("ERROR: cannot read 'keys' file [$! $hostname]") and exit (1);
|
or do_log ("ERROR: cannot read 'keys' file [$!/$hostname]") and exit (1);
|
||||||
do_log ("INFO: reading public keys from file: $key_file");
|
do_log ("INFO: reading public keys from file: $key_file");
|
||||||
while (<KEYS>) {
|
while (<KEYS>) {
|
||||||
|
|
||||||
@ -390,7 +493,7 @@ foreach my $key_file (@key_files) {
|
|||||||
|
|
||||||
chomp ();
|
chomp ();
|
||||||
next if (/^$/ || /\#/);
|
next if (/^$/ || /\#/);
|
||||||
|
|
||||||
# check for blacklisting
|
# check for blacklisting
|
||||||
my $key_line = $_;
|
my $key_line = $_;
|
||||||
if (grep (/\Q${key_line}\E/, @blacklist)) {
|
if (grep (/\Q${key_line}\E/, @blacklist)) {
|
||||||
@ -412,19 +515,19 @@ print Dumper(\%keys) if $debug;
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# read access definitions
|
# read access definitions
|
||||||
# result: %access (hash of arrays). The keys are the accounts for which
|
# result: %access (hash of arrays). The keys are the accounts for which
|
||||||
# access control has been defined for this server. The values are an array
|
# access control has been defined for this server. The values are an array
|
||||||
# with all the people who can access the account.
|
# with all the people who can access the account.
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
do_log ("INFO: reading 'access' file ...");
|
do_log ("INFO: reading 'access' file ...");
|
||||||
|
|
||||||
open (ACCESS, "<", "${run_dir}/access")
|
open (ACCESS, "<", "${run_dir}/access")
|
||||||
or do_log ("ERROR: cannot read 'access' file [$! $hostname]") and exit (1);
|
or do_log ("ERROR: cannot read 'access' file [$!/$hostname]") and exit (1);
|
||||||
while (<ACCESS>) {
|
while (<ACCESS>) {
|
||||||
|
|
||||||
my ($who, $where, $what, @who, @where, @what);
|
my ($who, $where, $what, @who, @where, @what);
|
||||||
|
|
||||||
chomp ();
|
chomp ();
|
||||||
next if (/^$/ || /\#/);
|
next if (/^$/ || /\#/);
|
||||||
s/\s+//g;
|
s/\s+//g;
|
||||||
@ -437,14 +540,14 @@ while (<ACCESS>) {
|
|||||||
do_log ("WARN: ignoring line $. in 'access' due to missing/non-resolving values [$hostname]");
|
do_log ("WARN: ignoring line $. in 'access' due to missing/non-resolving values [$hostname]");
|
||||||
next;
|
next;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach my $account (sort (@what)) {
|
foreach my $account (sort (@what)) {
|
||||||
|
|
||||||
my @new_array;
|
my @new_array;
|
||||||
|
|
||||||
foreach my $server (sort (@where)) {
|
foreach my $server (sort (@where)) {
|
||||||
foreach my $person (sort (@who)) {
|
foreach my $person (sort (@who)) {
|
||||||
do_log ("DEBUG: adding access for $account to $person on $server in \%access")
|
do_log ("DEBUG: adding access for $account to $person on $server in \%access")
|
||||||
if ($server eq $hostname);
|
if ($server eq $hostname);
|
||||||
# add person to access list if the entry is for this host
|
# add person to access list if the entry is for this host
|
||||||
push (@new_array, $person) if ($server eq $hostname);
|
push (@new_array, $person) if ($server eq $hostname);
|
||||||
@ -473,11 +576,11 @@ if ($preview && $global) {
|
|||||||
do_log ("INFO: display GLOBAL configuration ....");
|
do_log ("INFO: display GLOBAL configuration ....");
|
||||||
|
|
||||||
open (ACCESS, "<", "${run_dir}/access")
|
open (ACCESS, "<", "${run_dir}/access")
|
||||||
or do_log ("ERROR: cannot read 'access' file [$! $hostname]") and exit (1);
|
or do_log ("ERROR: cannot read 'access' file [$!/$hostname]") and exit (1);
|
||||||
while (<ACCESS>) {
|
while (<ACCESS>) {
|
||||||
|
|
||||||
my ($who, $where, $what, @who, @where, @what);
|
my ($who, $where, $what, @who, @where, @what);
|
||||||
|
|
||||||
chomp ();
|
chomp ();
|
||||||
next if (/^$/ || /\#/);
|
next if (/^$/ || /\#/);
|
||||||
s/\s+//g;
|
s/\s+//g;
|
||||||
@ -490,14 +593,14 @@ if ($preview && $global) {
|
|||||||
do_log ("WARN: ignoring line $. in 'access' due to missing/non-resolving values [$hostname]");
|
do_log ("WARN: ignoring line $. in 'access' due to missing/non-resolving values [$hostname]");
|
||||||
next;
|
next;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach my $account (sort (@what)) {
|
foreach my $account (sort (@what)) {
|
||||||
|
|
||||||
my @new_array;
|
my @new_array;
|
||||||
|
|
||||||
foreach my $server (sort (@where)) {
|
foreach my $server (sort (@where)) {
|
||||||
foreach my $person (sort (@who)) {
|
foreach my $person (sort (@who)) {
|
||||||
do_log ("$person|$server|$account")
|
do_log ("$person|$server|$account")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -508,7 +611,8 @@ if ($preview && $global) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# distribute keys into authorized_keys files in $access_dir
|
# distribute keys into authorized_keys files
|
||||||
|
# (defined by $key_location and/or $access_dir)
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
do_log ("INFO: applying SSH access rules ....");
|
do_log ("INFO: applying SSH access rules ....");
|
||||||
@ -523,7 +627,7 @@ unless ($preview) {
|
|||||||
if ($selinux_status eq "Permissive" or $selinux_status eq "Enforcing") {
|
if ($selinux_status eq "Permissive" or $selinux_status eq "Enforcing") {
|
||||||
do_log ("INFO: runtime info: detected active SELinux system on $hostname");
|
do_log ("INFO: runtime info: detected active SELinux system on $hostname");
|
||||||
$has_selinux = 1;
|
$has_selinux = 1;
|
||||||
}
|
}
|
||||||
# figure out RHEL version (via lsb_release or /etc/redhat-release)
|
# figure out RHEL version (via lsb_release or /etc/redhat-release)
|
||||||
$linux_version = qx#/usr/bin/lsb_release -rs 2>/dev/null | /usr/bin/cut -f1 -d'.'#;
|
$linux_version = qx#/usr/bin/lsb_release -rs 2>/dev/null | /usr/bin/cut -f1 -d'.'#;
|
||||||
chomp ($linux_version);
|
chomp ($linux_version);
|
||||||
@ -539,15 +643,27 @@ unless ($preview) {
|
|||||||
$release_string =~ m/release 6/i && do {
|
$release_string =~ m/release 6/i && do {
|
||||||
$linux_version = 6;
|
$linux_version = 6;
|
||||||
last SWITCH_RELEASE;
|
last SWITCH_RELEASE;
|
||||||
};
|
};
|
||||||
$release_string =~ m/release 7/i && do {
|
$release_string =~ m/release 7/i && do {
|
||||||
$linux_version = 7;
|
$linux_version = 7;
|
||||||
last SWITCH_RELEASE;
|
last SWITCH_RELEASE;
|
||||||
};
|
};
|
||||||
|
$release_string =~ m/release 8/i && do {
|
||||||
|
$linux_version = 8;
|
||||||
|
last SWITCH_RELEASE;
|
||||||
|
};
|
||||||
|
$release_string =~ m/release 9/i && do {
|
||||||
|
$linux_version = 9;
|
||||||
|
last SWITCH_RELEASE;
|
||||||
|
};
|
||||||
|
$release_string =~ m/release 10/i && do {
|
||||||
|
$linux_version = 8;
|
||||||
|
last SWITCH_RELEASE;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
# use fall back in case we cannot determine the version
|
# use fall back in case we cannot determine the version
|
||||||
if (not (defined ($linux_version)) or $linux_version eq "") {
|
if (not (defined ($linux_version)) or $linux_version eq "") {
|
||||||
$selinux_context = 'etc_t';
|
$selinux_context = 'etc_t';
|
||||||
$linux_version = 'unknown';
|
$linux_version = 'unknown';
|
||||||
} else {
|
} else {
|
||||||
@ -557,25 +673,70 @@ unless ($preview) {
|
|||||||
do_log ("INFO: runtime info: OS major version $linux_version, SELinux context $selinux_context on $hostname");
|
do_log ("INFO: runtime info: OS major version $linux_version, SELinux context $selinux_context on $hostname");
|
||||||
} else {
|
} else {
|
||||||
do_log ("INFO: runtime info: OS major version $linux_version on $hostname");
|
do_log ("INFO: runtime info: OS major version $linux_version on $hostname");
|
||||||
}
|
}
|
||||||
last SWITCH_OS;
|
last SWITCH_OS;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# only add authorized_keys for existing accounts,
|
# only add authorized_keys for existing accounts,
|
||||||
# otherwise revoke access if needed
|
# otherwise revoke access if needed
|
||||||
foreach my $account (sort (@accounts)) {
|
SET_KEY: foreach my $account (sort (@accounts)) {
|
||||||
|
|
||||||
my $access_file = "$access_dir/$account";
|
my ($access_file, $authorizedkeys_file, $uid, $gid, $home_dir, $login_shell) = (undef, undef, undef, undef, undef, undef);
|
||||||
|
|
||||||
# only add authorised_keys if there are access definitions
|
# set $access_file when using SSH controls logic
|
||||||
|
if ($key_location eq 'use_sshd' and defined ($authorizedkeys_option)) {
|
||||||
|
# use sshd logic (replacing %u,%h, %%)
|
||||||
|
$authorizedkeys_file = $authorizedkeys_option;
|
||||||
|
$authorizedkeys_file =~ s/%u/$account/g;
|
||||||
|
$authorizedkeys_file =~ s/%h/$hostname/g;
|
||||||
|
$authorizedkeys_file =~ s/%%/%/g;
|
||||||
|
# check relative path (assume $HOME needs to be added)
|
||||||
|
if ($authorizedkeys_file !~ /^\//) {
|
||||||
|
($uid, $gid, $home_dir, $login_shell) = (getpwnam($account))[2,3,7,8];
|
||||||
|
# do not accept invalid $HOME or shells
|
||||||
|
if (defined ($home_dir)) {
|
||||||
|
if (grep( /^$home_dir$/, @disallowed_homes) or grep( /^$login_shell/, @disallowed_shells)) {
|
||||||
|
do_log ("DEBUG: invalid HOME or SHELL for $account [$hostname]");
|
||||||
|
next SET_KEY;
|
||||||
|
} else {
|
||||||
|
$authorizedkeys_file = $home_dir."/".$authorizedkeys_file;
|
||||||
|
do_log ("DEBUG: adding $home_dir to public key path for $account [$hostname]");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
do_log ("ERROR: unable to get HOME for $account [$hostname]");
|
||||||
|
next SET_KEY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$access_file = $authorizedkeys_file;
|
||||||
|
} else {
|
||||||
|
# use native SSH controls logic
|
||||||
|
$access_file = "$access_dir/$account";
|
||||||
|
}
|
||||||
|
do_log ("DEBUG: public key location for $account resolves to $access_file [$hostname]");
|
||||||
|
|
||||||
|
# only add authorised_keys if there are access definitions
|
||||||
if ($access{$account}) {
|
if ($access{$account}) {
|
||||||
|
|
||||||
unless ($preview) {
|
unless ($preview) {
|
||||||
|
# create $HOME/.ssh if needed but only when we are root
|
||||||
|
if ($key_location eq 'use_sshd' and defined ($authorizedkeys_option)) {
|
||||||
|
if ($> == 0) {
|
||||||
|
if (! -d "$home_dir/.ssh") {
|
||||||
|
mkdir ("$home_dir/.ssh", 0700)
|
||||||
|
or do_log "ERROR: failed to create the $home_dir/.ssh directory [$!/$hostname]"
|
||||||
|
and next SET_KEY;
|
||||||
|
chown ($uid, $gid, "$home_dir/.ssh")
|
||||||
|
or do_log "ERROR: failed to set onwerships on the $home_dir/.ssh directory [$!/$hostname]"
|
||||||
|
and next SET_KEY;
|
||||||
|
do_log ("DEBUG: created directory $home_dir/.ssh for $account [$hostname]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
open (KEYFILE, "+>", $access_file)
|
open (KEYFILE, "+>", $access_file)
|
||||||
or do_log ("ERROR: cannot open file for writing in $access_dir [$! $hostname]")
|
or do_log ("ERROR: cannot open file for writing at $access_file [$!/$hostname]")
|
||||||
and exit (1);
|
and next SET_KEY;
|
||||||
}
|
}
|
||||||
foreach my $person (sort (@{$access{$account}})) {
|
foreach my $person (sort (@{$access{$account}})) {
|
||||||
my $real_name = $person;
|
my $real_name = $person;
|
||||||
@ -583,26 +744,30 @@ foreach my $account (sort (@accounts)) {
|
|||||||
# only add authorized_keys if $person actually has a key
|
# only add authorized_keys if $person actually has a key
|
||||||
if (exists ($keys{$person})) {
|
if (exists ($keys{$person})) {
|
||||||
# only add authorized_keys if $person actually has an account
|
# only add authorized_keys if $person actually has an account
|
||||||
print KEYFILE "$keys{$person}{keytype} $keys{$person}{key} $real_name\n"
|
print KEYFILE "$keys{$person}{keytype} $keys{$person}{key} $real_name\n"
|
||||||
unless $preview;
|
unless $preview;
|
||||||
do_log ("INFO: granting access to $account for $real_name on $hostname");
|
do_log ("INFO: granting access to $account for $real_name on $hostname");
|
||||||
} else {
|
} else {
|
||||||
do_log ("INFO: denying access (no key) to $account for $real_name on $hostname");
|
do_log ("INFO: denying access (no key) to $account for $real_name on $hostname");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
close (KEYFILE) unless $preview;
|
close (KEYFILE) unless $preview;
|
||||||
|
|
||||||
# set permissions to world readable and check for SELinux context
|
# set ownerships/permissions on public key file and check for SELinux context
|
||||||
unless ($preview) {
|
unless ($preview) {
|
||||||
set_file ($access_file, 0644, 0, 0);
|
if ($key_location eq 'use_controls') {
|
||||||
|
set_file ($access_file, 0644, 0, 0);
|
||||||
|
} else {
|
||||||
|
set_file ($access_file, 0600, $uid, $gid);
|
||||||
|
}
|
||||||
# selinux labels
|
# selinux labels
|
||||||
SWITCH: {
|
SWITCH: {
|
||||||
$os eq "Linux" && do {
|
$os eq "Linux" && do {
|
||||||
if ($has_selinux) {
|
if ($has_selinux) {
|
||||||
system ("/usr/bin/chcon -t $selinux_context $access_file") and
|
system ("/usr/bin/chcon -t $selinux_context $access_file") and
|
||||||
do_log ("WARN: failed to set SELinux context $selinux_context on $access_file [$hostname]");
|
do_log ("WARN: failed to set SELinux context $selinux_context on $access_file [$hostname]");
|
||||||
};
|
};
|
||||||
last SWITCH;
|
last SWITCH;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -611,7 +776,7 @@ foreach my $account (sort (@accounts)) {
|
|||||||
if (-f $access_file) {
|
if (-f $access_file) {
|
||||||
unless ($preview) {
|
unless ($preview) {
|
||||||
unlink ($access_file)
|
unlink ($access_file)
|
||||||
or do_log ("ERROR: cannot remove obsolete access file(s) [$! $hostname]")
|
or do_log ("ERROR: cannot remove obsolete access file $access_file [$!/$hostname]")
|
||||||
and exit (1);
|
and exit (1);
|
||||||
} else {
|
} else {
|
||||||
do_log ("INFO: removing obsolete access $access_file on $hostname");
|
do_log ("INFO: removing obsolete access $access_file on $hostname");
|
||||||
@ -621,32 +786,35 @@ foreach my $account (sort (@accounts)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# alert on/remove extraneous authorized_keys files
|
# alert on/remove extraneous authorized_keys files (SSH controls logic only)
|
||||||
# (access files for which no longer a valid UNIX account exists)
|
# (access files for which no longer a valid UNIX account exists)
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
do_log ("INFO: checking for extraneous access files ....");
|
if ($key_location eq 'use_controls') {
|
||||||
|
|
||||||
opendir (ACCESS_DIR, $access_dir)
|
do_log ("INFO: checking for extraneous access files ....");
|
||||||
or do_log ("ERROR: cannot open directory $access_dir [$! $hostname]")
|
|
||||||
|
opendir (ACCESS_DIR, $access_dir)
|
||||||
|
or do_log ("ERROR: cannot open directory $access_dir [$!/$hostname]")
|
||||||
and exit (1);
|
and exit (1);
|
||||||
while (my $access_file = readdir (ACCESS_DIR)) {
|
while (my $access_file = readdir (ACCESS_DIR)) {
|
||||||
next if ($access_file =~ /^\./);
|
next if ($access_file =~ /^\./);
|
||||||
unless (grep (/$access_file/, @accounts)) {
|
unless (grep (/$access_file/, @accounts)) {
|
||||||
do_log ("WARN: found extraneous access file in $access_dir/$access_file [$hostname]");
|
do_log ("WARN: found extraneous access file in $access_dir/$access_file [$hostname]");
|
||||||
push (@zombie_files, "$access_dir/$access_file");
|
push (@zombie_files, "$access_dir/$access_file");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
closedir (ACCESS_DIR);
|
||||||
closedir (ACCESS_DIR);
|
do_log ("INFO: ".scalar (@zombie_files)." extraneous access file(s) found on $hostname");
|
||||||
do_log ("INFO: ".scalar (@zombie_files)." extraneous access file(s) found on $hostname");
|
print Dumper (\@zombie_files) if $debug;
|
||||||
print Dumper (\@zombie_files) if $debug;
|
|
||||||
|
|
||||||
# remove if requested and needed
|
# remove if requested and needed
|
||||||
if ($remove && @zombie_files) {
|
if ($remove && @zombie_files) {
|
||||||
my $count = unlink (@zombie_files)
|
my $count = unlink (@zombie_files)
|
||||||
or do_log ("ERROR: cannot remove extraneous access file(s) [$! $hostname]")
|
or do_log ("ERROR: cannot remove extraneous access file(s) [$!/$hostname]")
|
||||||
and exit (1);
|
and exit (1);
|
||||||
do_log ("INFO: $count extraneous access files removed $hostname");
|
do_log ("INFO: $count extraneous access files removed $hostname");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exit (0);
|
exit (0);
|
||||||
@ -667,20 +835,23 @@ update_ssh.pl - distributes SSH public keys in a desired state model.
|
|||||||
|
|
||||||
=head1 SYNOPSIS
|
=head1 SYNOPSIS
|
||||||
|
|
||||||
update_ssh.pl[-d|--debug]
|
update_ssh.pl[-d|--debug]
|
||||||
[-h|--help]
|
[-h|--help]
|
||||||
|
[-i|--ignore]
|
||||||
([-p|--preview] [-g|--global]) | [-r|--remove]
|
([-p|--preview] [-g|--global]) | [-r|--remove]
|
||||||
[-v|--verbose]
|
[-v|--verbose]
|
||||||
[-V|--version]
|
[-V|--version]
|
||||||
|
|
||||||
|
|
||||||
=head1 DESCRIPTION
|
=head1 DESCRIPTION
|
||||||
|
|
||||||
B<update_ssh.pl> distributes SSH keys to the appropriate files (.e. 'authorized_keys') into the C<$access_dir> repository based on the F<access>, F<alias> and F<keys> files.
|
B<update_ssh.pl> distributes SSH keys to the appropriate files (.e. 'authorized_keys') into the C<$access_dir> repository based on the F<access>, F<alias> and F<keys> files.
|
||||||
This script should be run on each host where SSH key authentication is the exclusive method of (remote) authentication.
|
Alternatively B<update_ssh.pl> can distribute public keys to the location specified in the AuthorizedkeysFile setting of F<sshd_config> (allowing public keys to be distributed
|
||||||
|
to the traditional location in a user's HOME directory). See C<key_location> setting in F<update_ssh.conf[.local]>for more information.
|
||||||
|
This script should be run on each host where SSH key authentication is the exclusive method of (remote) authentication.
|
||||||
|
|
||||||
For update SSH public keys must be stored in a generic F<keys> file within the same directory as B<update_ssh.pl> script.
|
Orginally SSH public keys must be stored in a generic F<keys> file within the same directory as B<update_ssh.pl> script.
|
||||||
Alternatively key files may be stored as set of individual key files within a called sub-directory called F<keys.d>.
|
Alternatively key files may be stored as set of individual key files within a called sub-directory called F<keys.d>.
|
||||||
Both methods are mutually exclusive and the latter always take precedence.
|
Both methods are mutually exclusive and the latter always take precedence.
|
||||||
|
|
||||||
=head1 CONFIGURATION
|
=head1 CONFIGURATION
|
||||||
@ -693,7 +864,7 @@ B<update_ssh.pl> requires the presence of at least one of the following configur
|
|||||||
|
|
||||||
=item * F<update_ssh.conf.local>
|
=item * F<update_ssh.conf.local>
|
||||||
|
|
||||||
=back
|
=back
|
||||||
|
|
||||||
Use F<update_ssh.conf.local> for localized settings per host. Settings in the localized configuration file will always override other values.
|
Use F<update_ssh.conf.local> for localized settings per host. Settings in the localized configuration file will always override other values.
|
||||||
|
|
||||||
@ -703,8 +874,12 @@ Following settings must be configured:
|
|||||||
|
|
||||||
=item * B<use_fqdn> : whether to use short or FQDN host names
|
=item * B<use_fqdn> : whether to use short or FQDN host names
|
||||||
|
|
||||||
|
=item * B<ignore_errors> : whether to ignore errors during key deployment
|
||||||
|
|
||||||
=item * B<access_dir> : target directory for allowed SSH public key files
|
=item * B<access_dir> : target directory for allowed SSH public key files
|
||||||
|
|
||||||
|
=item * B<key_location> : whether or not to use AuthorizedkeysFile setting in sshd_config for overriding $access_dir
|
||||||
|
|
||||||
=item * B<blacklist_file> : location of the file with blacklisted SSH public keys
|
=item * B<blacklist_file> : location of the file with blacklisted SSH public keys
|
||||||
|
|
||||||
=back
|
=back
|
||||||
@ -712,7 +887,7 @@ Following settings must be configured:
|
|||||||
=head1 BLACKLISTING
|
=head1 BLACKLISTING
|
||||||
|
|
||||||
Key blacklisting can be performed by adding a public key definition in its entirety to the blacklist keys file. When a blacklisted key is
|
Key blacklisting can be performed by adding a public key definition in its entirety to the blacklist keys file. When a blacklisted key is
|
||||||
found in the available F<keys> file(s) during SSH control updates, an alert will be shown on STDOUT and the key will be ignored for the rest.
|
found in the available F<keys> file(s) during SSH controls updates, an alert will be shown on STDOUT and the key will be ignored for the rest.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
@ -731,11 +906,15 @@ S< >Be I<very> verbose during execution; show array/hash dumps.
|
|||||||
|
|
||||||
S< >Show the help page.
|
S< >Show the help page.
|
||||||
|
|
||||||
|
=item -i | --ignore
|
||||||
|
|
||||||
|
S< >Ignore errors during key deployment.
|
||||||
|
|
||||||
=item -p | --preview
|
=item -p | --preview
|
||||||
|
|
||||||
S< >Do not actually distribute any SSH public keys, nor update/remove any 'authorized_keys' files.
|
S< >Do not actually distribute any SSH public keys, nor update/remove any 'authorized_keys' files.
|
||||||
|
|
||||||
=item -p | --global
|
=item -g | --global
|
||||||
|
|
||||||
S< >Must be used in conjunction with the --preview option. This will dump the global namespace/configuration to STDOUT.
|
S< >Must be used in conjunction with the --preview option. This will dump the global namespace/configuration to STDOUT.
|
||||||
|
|
||||||
@ -746,12 +925,12 @@ S< >Remove any extraneous 'authorized_keys' files (i.e. belonging to non-e
|
|||||||
=item -v | --verbose
|
=item -v | --verbose
|
||||||
|
|
||||||
S< >Be verbose during exection.
|
S< >Be verbose during exection.
|
||||||
|
|
||||||
=item -V | --version
|
=item -V | --version
|
||||||
|
|
||||||
S< >Show version of the script.
|
S< >Show version of the script.
|
||||||
|
|
||||||
=back
|
=back
|
||||||
|
|
||||||
=head1 NOTES
|
=head1 NOTES
|
||||||
|
|
||||||
@ -761,16 +940,8 @@ S< >Show version of the script.
|
|||||||
|
|
||||||
=item * Options may be bundled (e.g. -vp)
|
=item * Options may be bundled (e.g. -vp)
|
||||||
|
|
||||||
=back
|
=back
|
||||||
|
|
||||||
=head1 AUTHOR
|
=head1 AUTHOR
|
||||||
|
|
||||||
(c) KUDOS BVBA, Patrick Van der Veken
|
(c) KUDOS BV, 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, new config option 'selinux_context' [Patrick Van der Veken]
|
|
||||||
@(#) 2015-08-08: VRF 1.0.2: small fix for 'cut' command [Patrick Van der Veken]
|
|
||||||
@(#) 2015-08-15: 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.2.0: replace read of /etc/passwd by pwgetent() call, small and not so small fixes [Patrick Van der Veken]
|
|
Loading…
x
Reference in New Issue
Block a user