Initial commit

This commit is contained in:
Stéphane Lesimple 2020-10-15 16:32:37 +00:00
commit fde20136ef
No known key found for this signature in database
GPG key ID: 4B4A3289E9D35658
463 changed files with 39401 additions and 0 deletions

4
.dockerignore Normal file
View file

@ -0,0 +1,4 @@
.git
doc
docs
*.tar.gz

32
.github/workflows/documentation.yml vendored Normal file
View file

@ -0,0 +1,32 @@
name: documentation
on:
push:
branches:
- master
jobs:
publish:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Install sphinx and prerequisites
run: |
sudo apt update
sudo apt install -y python3-sphinx-rtd-theme python3-sphinx make libcommon-sense-perl libjson-perl
-
name: Build documentation
run: cd doc/sphinx/ && make all
-
name: Deploy to GitHub Pages
if: success()
uses: crazy-max/ghaction-github-pages@v2
with:
target_branch: gh-pages
build_dir: docs
allow_empty_commit: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

29
.github/workflows/tests.yml vendored Normal file
View file

@ -0,0 +1,29 @@
name: Linux distros tests
on:
pull_request:
types: [labeled, synchronize]
jobs:
tests_full:
strategy:
matrix:
platform: [centos7, centos8, debian10, debian8, debian9, opensuse15, opensuse151, ubuntu1404, ubuntu1604, ubuntu1804, ubuntu2004]
runs-on: ubuntu-latest
if: contains(github.event.pull_request.labels.*.name, 'tests:full')
steps:
- uses: actions/checkout@v2
- name: run tests inside a ${{ matrix.platform }} docker
run: tests/functional/docker/docker_build_and_run_tests.sh ${{ matrix.platform }}
env:
DOCKER_TTY: false
tests_short:
runs-on: ubuntu-latest
if: contains(github.event.pull_request.labels.*.name, 'tests:short')
steps:
- uses: actions/checkout@v2
- name: run tests inside a debian10 docker
run: tests/functional/docker/docker_build_and_run_tests.sh debian10
env:
DOCKER_TTY: false

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
doc/sphinx/_build

12
AUTHORS Normal file
View file

@ -0,0 +1,12 @@
# This is the official list of OVH::Bastion authors for copyright purposes.
# This file is distinct from the CONTRIBUTORS files
# and it lists the copyright holders only.
# Names should be added to this file as one of
# Organization's name
# Individual's name <submission email address>
# Individual's name <submission email address> <email2> <emailN>
# Please keep the list sorted.
OVH SAS

83
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,83 @@
# Contributing to The Bastion
This project accepts contributions. In order to contribute, you should
pay attention to a few things:
1. your code must follow the The Bastion design choices, see DESIGN.md
2. your code must follow the coding style rules
3. your code must be added to the unit and/or integration tests where applicable
4. your code must be documented
5. your work must be signed (see below)
6. you may contribute through GitHub Pull Requests
# Coding and documentation Style for source code
- All languages
- Code must be indented with 4-spaces, no tabs. Vim modelines are present
in all source files, so if you use vim, you should be good to go
- Perl
- Code must be tidy (see `bin/dev/perl-tidy.sh`)
- Code must not raise any perlcritic warning (see `bin/dev/perl-critic.sh`)
- One must refrain using any non-core Perl module (check `corelist`)
- If not possible, the module should be packaged at least under Debian,
all supported versions, and available at least in trusted third party
repositories on other supported OSes. No `cpan install`.
- POSIX shell and Bash
- Code must not raise any shellcheck warning (see `bin/dev/shell-check.sh`)
# Submitting Modifications
The contributions should be submitted through Github Pull Requests
and follow the DCO which is defined below.
# Licensing for new files
The Bastion is licensed under the Apache License 2.0. Anything
contributed to The Bastion must be released under this license.
When introducing a new file into the project, please make sure it has a
copyright header making clear under which license it's being released.
# Developer Certificate of Origin (DCO)
To improve tracking of contributions to this project we will use a
process modeled on the modified DCO 1.1 and use a "sign-off" procedure
on patches that are being emailed around or contributed in any other
way.
The sign-off is a simple line at the end of the explanation for the
patch, which certifies that you wrote it or otherwise have the right
to pass it on as an open-source patch. The rules are pretty simple,
if you can certify the below:
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I have
the right to submit it under the open source license indicated in
the file; or
(b) The contribution is based upon previous work that, to the best of
my knowledge, is covered under an appropriate open source License
and I have the right under that license to submit that work with
modifications, whether created in whole or in part by me, under
the same open source license (unless I am permitted to submit
under a different license), as indicated in the file; or
(c) The contribution was provided directly to me by some other person
who certified (a), (b) or (c) and I have not modified it.
(d) The contribution is made free of any other party's intellectual
property claims or rights.
(e) I understand and agree that this project and the contribution are
public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
then you just add a line saying
Signed-off-by: Random J Developer <random@example.org>
using your real name (sorry, no pseudonyms or anonymous contributions.)

15
CONTRIBUTORS Normal file
View file

@ -0,0 +1,15 @@
# This is the official list of people who can contribute
# (and typically have contributed) code to the OVH::Bastion repository.
#
# Names should be added to this file only after verifying that
# the individual or the individual's organization has agreed to
# the appropriate CONTRIBUTING.md file.
#
# Names should be added to this file like so:
# Individual's name <submission email address>
# Individual's name <submission email address>
#
# Please keep the list sorted.
#
Adrien Barreau <adrien.barreau@ovhcloud.com>
Stéphane Lesimple <stephane.lesimple@ovhcloud.com>

40
DESIGN.md Normal file
View file

@ -0,0 +1,40 @@
# The Bastion design choices
This document aims to summarize a few design choices that have been made
on this project, that dictate how features are implemented.
## Use the well trusted and existing UNIX building blocks, don't recode them
The Bastion heavily relies on well known and trusted system blocks to work.
All the SSH part is completely handled by OpenSSH server and client programs.
The MFA mechanism also heavily relies on PAM.
## The OS as a safety net for buggy or exploitable code
A bastion functional user is always mapped to an actual operating system user.
Same goes for bastion groups: they're mapped to actual OS groups.
This is also true for group roles: gatekeeper, owner and aclkeeper roles are
mapped to system groups.
Private keys of an account are only readable by the corresponding operating
system user, and same goes for the group private keys. This way, even if the
code is tricked to allow access when it shouldn't have (flawed logic or bug),
then the OS will still deny reading the key file.
This concept has been explained in the ([https://www.ovh.com/blog/the-bastion-part-3-security-at-the-core/](Blog Post #3 - Security at the Core))
## Zero trust between portions of code running at different permission levels
Most of The Bastion code is running under the unprivileged system user
corresponding to the actual user of the bastion. When some code needs to
run with privileges, for example to be able to create an account, a first
portion of the code checks for the validity of the request first, under the
same privileges than the user, this is called `a plugin`.
To actually create the system user, `sudo` is used to run just a specific
portion of the code. Such portions of code are named `helpers`, and always
run under perl tainted mode.
Helpers communicate back their result using JSON, which is then read from
the plugin (the unprivileged portion of code), and parsed.
This concept has been explained in the ([https://www.ovh.com/blog/the-bastion-part-3-security-at-the-core/](Blog Post #3 - Security at the Core))

192
LICENSE Normal file
View file

@ -0,0 +1,192 @@
Copyright 2020 OVHcloud
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
A copy of the license terms follows:
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

12
MAINTAINERS Normal file
View file

@ -0,0 +1,12 @@
# This is the official list of the project maintainers.
# This is mostly useful for contributors that want to push
# significant pull requests or for project management issues.
#
#
# Names should be added to this file like so:
# Individual's name <submission email address>
# Individual's name <submission email address>
#
# Please keep the list sorted.
#
Stéphane Lesimple <stephane.lesimple@ovhcloud.com>

149
README.md Normal file
View file

@ -0,0 +1,149 @@
The Bastion
===========
Bastions are a cluster of machines used as the unique entry point by operational teams (such as sysadmins, developers, database admins, ...) to securely connect to devices (servers, virtual machines, cloud instances, network equipment, ...), usually using `ssh`.
Bastions provides mechanisms for authentication, authorization, traceability and auditability for the whole infrastructure.
Learn more by reading the blog post series that announced the release:
- [https://www.ovh.com/blog/the-ovhcloud-bastion-part-1/](Part 1 - Genesis)
- [https://www.ovh.com/blog/the-ovhcloud-ssh-bastion-part-2-delegation-dizziness/](Part 2 - Delegation Dizziness)
- [https://www.ovh.com/blog/the-bastion-part-3-security-at-the-core/](Part 3 - Security at the Core)
- [https://www.ovh.com/blog/the-bastion-part-4-open-sourcing/](Part 4 - Open Sourcing)
## Installing, upgrading, using The Bastion
Please see the online documentation ([https://ovh.github.io/the-bastion](https://ovh.github.io/the-bastion)), or the corresponding text-based documentation which can be found in the `doc/` folder.
## TL;DR
### Testing it with Docker
Let's build the docker image and run it
docker build -f docker/Dockerfile.debian10 -t bastion:debian10 .
docker run -d -p 22 --name bastiontest bastion:debian10
Configure the first administrator account (get your public SSH key ready)
docker exec -it bastiontest /opt/bastion/bin/admin/setup-first-admin-account.sh poweruser auto
We're now up and running with the default configuration! Let's setup a handy bastion alias, and test the `info` command:
PORT=$(docker port bastiontest | cut -d: -f2)
alias bastion="ssh poweruser@127.0.0.1 -tp $PORT -- "
bastion --osh info
It should greet you as being a bastion admin, which means you have access to all commands. Let's enter interactive mode:
bastion -i
This is useful to call several `--osh` plugins in a row. Now we can ask for help to see all plugins:
$> help
If you have a remote machine you want to try to connect to through the bastion, fetch your egress key:
$> selfListEgressKeys
Copy this public key to the remote machine's `authorized_keys` under the `.ssh/` folder of the account you want to connect to, then:
$> selfAddPersonalAccess --host <remote_host> --user <remote_account_name> --port-any
$> ssh <remote_account_name>@<remote_host>
Note that you can connect directly without using interactive mode, with:
bastion <remote_account_name>@<remote_machine_host_or_ip>
That's it! Additional documentation is available under the `doc/` folder and online ([https://ovh.github.io/the-bastion](https://ovh.github.io/the-bastion)).
Be sure to check the help of the bastion (`bastion --help`) and the help of each osh plugin (`bastion --osh command --help`)
Also don't forget to customize your `bastion.conf` file, which can be found in `/etc/bastion/bastion.conf` (for Linux)
## Compatibility
Linux distros below are tested with each release, but as this is a security product, you are *warmly* advised to run it on the latest up-to-date stable version of your favorite OS:
- Debian 10 (Buster), 9 (Stretch), 8 (Jessie)
- RHEL/CentOS 8, 7
- Ubuntu LTS 20.04, 18.04, 16.04, 14.04*
- OpenSUSE Leap 15.1*, 15*
*: Note that these versions have no MFA support.
Any other so-called "modern" Linux version are not tested with each release, but should work with no or minor adjustments.
The code is also known to work correctly under:
- FreeBSD 10+ / HardenedBSD [no MFA support]
Other BSD variants partially work but are unsupported and discouraged as they have a severe limitation over the maximum number of supplementary groups (causing problems for group membership and restricted commands checks), no filesystem-level ACL support and missing MFA:
- OpenBSD 5.4+
- NetBSD 7+
## Reliability
When hell is breaking loose on all your infrastructures and/or your network, bastions still need to be the last component standing because you need them to access the rest of your infrastructure... to be able to actually fix the problem. Hence reliability is key.
* The KISS principle is used where possible for design and code: less complicated code means more auditability and less bugs
* Only a few well-known libraries are used, less third party code means a tinier attack surface
* The bastion is engineered to be self-sufficient: less dependencies such as databases, other daemons, or other machines, statistically means less downtime
* High availability can be setup so that multiple bastion instances form a cluster of several instances, with any instance usable at all times (active/active scheme)
# Code quality
* The code is ran under `perltidy`
* The code is also ran under `perlcritic`
* Functional tests are used before every release
## Security at the core
Even with the most conservative, precautionous and paranoid coding process, code has bugs, so it shouldn't be trusted blindly. Hence the bastion doesn't trust its own code. It leverages the operating system security primitives to get additional security, as seen below.
- Uses the well-known and trusted UNIX Discretionary Access Control:
- Bastion users are mapped to actual system users
- Bastion groups are mapped to actual system groups
- All the code is constantly checking rights before allowing any action
- UNIX DAC is used as a safety belt to prevent an action from succeeding even if the code is tricked into allowing it
- The bastion main script is declared as the bastion user's system shell:
- No user has real (`bash`-like) shell access on the system
- All code is ran under the unprivileged user's system account rights
- Even if a user could escape to a real shell, he wouldn't be able to connect to machines he doesn't have access to, because he doesn't have filesystem-level read access to the SSH keys
- The code is modular
- The main code mainly checks rights, logs actions, and enable `ssh` access to other machines
- All side commands, called *plugins*, are in modules separated from the main code
- The modules can either be *open* or *restricted*
- Only accounts that have been specifically granted on a need-to-use basis can run a specific restricted plugin
- This is checked by the code, and also enforced by UNIX DAC (the plugin is only readable and executable by the system group specific to the plugin)
- All the code needing extended system privileges is separated from the main code, in modules called *helpers*
- Helpers are run exclusively under `sudo`
- The `sudoers` configuration is attached to a system group specific to the command, which is granted to accounts on a need-to-use basis
- The helpers are only readable and executable by the system group specific to the command
- The helpers path and some of their immutable parameters are hardcoded in the `sudoers` configuration
- Perl tainted mode (`-T`) is used for all code running under `sudo`, preventing any user-input to interfere with the logic, by halting execution immediately
- Code running under `sudo` doesn't trust its caller and re-checks every input
- Communication between unprivileged and privileged-code are done using JSON
## Auditability
- Bastion administrators must use the bastion's logic to connect to itself to administer it (or better, use another bastion to do so), this ensures auditability in all cases
* Every access and action (wether allowed or denied) is logged with:
* `syslog`, which should also be sent to a remote syslog server to ensure even bastion administrators can't tamper their tracks, and/or
* local `sqlite3` databases for easy searching
* This code is used in production in several PCI-DSS, ISO 27001, SOC1 and SOC2 certified environments
## License
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -0,0 +1,57 @@
#! /usr/bin/env bash
# vim: set filetype=sh ts=4 sw=4 sts=4 et:
set -e
basedir=$(readlink -f "$(dirname "$0")"/../..)
# shellcheck source=lib/shell/functions.inc
. "$basedir"/lib/shell/functions.inc
TTYREC_ARCHIVE_URL='https://github.com/ovh/ovh-ttyrec/archive/master.zip'
action_doing "Detecting OS..."
action_detail "Found $OS_FAMILY"
if [ "$OS_FAMILY" = Linux ]; then
action_detail "Found distro $LINUX_DISTRO version $DISTRO_VERSION (major $DISTRO_VERSION_MAJOR), distro like $DISTRO_LIKE"
fi
action_done
if echo "$DISTRO_LIKE" | grep -q -w debian; then
list="make gcc unzip wget"
if [ "$LINUX_DISTRO" = debian ] && [ "$DISTRO_VERSION_MAJOR" -ge 9 ]; then
list="$list libzstd-dev"
elif [ "$LINUX_DISTRO" = ubuntu ] && [ "$DISTRO_VERSION_MAJOR" -ge 16 ]; then
list="$list libzstd-dev"
fi
apt-get update
# shellcheck disable=SC2086
apt-get install -y $list
# shellcheck disable=SC2086
cleanup() {
apt-get remove --purge -y $list
apt-get autoremove --purge -y
}
elif echo "$DISTRO_LIKE" | grep -q -w rhel; then
yum install -y gcc make unzip wget
cleanup() { yum remove -y gcc make unzip wget; }
elif echo "$DISTRO_LIKE" | grep -q -w suse; then
zypper install -y gcc make libzstd-devel-static unzip wget
cleanup() { zypper remove -y -u gcc make libzstd-devel-static unzip wget; }
else
echo "This script doesn't support this OS yet ($DISTRO_LIKE)" >&2
exit 1
fi
cd /tmp
wget "$TTYREC_ARCHIVE_URL"
unzip master.zip
cd ovh-ttyrec-master
./configure
make
make install
cleanup
if ttyrec -V; then
action_done "ttyrec correctly installed"
else
action_error "couldn't install ttyrec"
fi

715
bin/admin/check-consistency.pl Executable file
View file

@ -0,0 +1,715 @@
#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;
use Data::Dumper;
use Term::ANSIColor;
use Digest::MD5 ();
use File::Basename;
my $BASEDIR = dirname(__FILE__) . '/../..';
my $MIN_KEYGROUP_GID = 2000;
my $MAX_KEYGROUP_GID = 99999;
my @KEY_GROUPS_IGNORE = qw{ keeper reader };
my $HOME_SUBDIRS_IGNORE_RE = qr{^^};
my $bad;
# generate a uniq prefix based on caller's lineno and caller's caller's lineno, useful to grep or grep -v
sub _prefix { return uc(unpack('H*', pack('S', (caller(1))[2])) . unpack('H*', pack('S', (caller(2))[2]))) . ": "; }
sub info { print $_[0] . "\n"; return 1; } ## no critic (RequireArgUnpacking)
sub _wrn { $bad++; print colored(_prefix() . $_[0], "blue") . "\n"; return 1; } ## no critic (RequireArgUnpacking,ProhibitUnusedPrivateSubroutine)
sub _err { $bad++; print colored(_prefix() . $_[0], "red") . "\n"; return 1; } ## no critic (RequireArgUnpacking)
sub _crit { $bad++; print colored(_prefix() . $_[0], "bold red") . "\n"; return 1; } ## no critic (RequireArgUnpacking)
# Linux and BSD don't always have the same account names for UID/GID 0
my ($UID0) = (qx{getent passwd 0})[0] =~ /^([^:]+)/; ## no critic (ProhibitBacktickOperators)
my ($GID0) = (qx{getent group 0})[0] =~ /^([^:]+)/; ## no critic (ProhibitBacktickOperators)
my $islinux = (($^O =~ /linux/i) ? 1 : 0);
my $hasacls = (($^O =~ /linux|freebsd/i) ? 1 : 0);
# get all the key* groups
my %keygroupsbyname = ();
my %aclkgroupsbyname = ();
my %gkgroupsbyname = ();
my %owgroupsbyname = ();
my %keygroupsbyid = ();
my %aclkgroupsbyid = ();
my %gkgroupsbyid = ();
my %owgroupsbyid = ();
my $sudoers_dir = '/etc/sudoers.d';
if (!-d $sudoers_dir && -d '/usr/pkg/etc/sudoers.d') {
$sudoers_dir = '/usr/pkg/etc/sudoers.d';
}
elsif (!-d $sudoers_dir && !$islinux) {
$sudoers_dir = '/usr/local/etc/sudoers.d';
}
_err "/nonexistent exists" if -e "/nonexistent";
open(my $fh_group, '<', '/etc/group') or die $!;
while (<$fh_group>) {
/^key([^:]+):[^:]+:(\d+)/ or next;
my $name = $1;
my $id = $2;
if (exists $keygroupsbyname{$name} or exists $gkgroupsbyname{$name} or exists $owgroupsbyname{$name} or exists $aclkgroupsbyname{$name}) {
_err "group $name already seen!";
}
if ($name =~ /-gatekeeper$/) {
$gkgroupsbyname{$name} = {name => $name, id => $id};
}
elsif ($name =~ /-aclkeeper$/) {
$aclkgroupsbyname{$name} = {name => $name, id => $id};
}
elsif ($name =~ /-owner$/) {
$owgroupsbyname{$name} = {name => $name, id => $id};
}
else {
$keygroupsbyname{$name} = {name => $name, id => $id};
}
if (exists $keygroupsbyid{$id} or exists $gkgroupsbyid{$id} or exists $owgroupsbyid{$id} or exists $aclkgroupsbyname{$id}) {
_crit "group $name 's ID already seen!";
}
if ($name =~ /-gatekeeper$/) {
$gkgroupsbyid{$id} = {name => $name, id => $id};
}
elsif ($name =~ /-aclkeeper$/) {
$aclkgroupsbyid{$id} = {name => $name, id => $id};
}
elsif ($name =~ /-owner$/) {
$owgroupsbyid{$id} = {name => $name, id => $id};
}
else {
$keygroupsbyid{$id} = {name => $name, id => $id};
}
if (grep { $name eq $_ } @KEY_GROUPS_IGNORE) {
delete $keygroupsbyname{$name};
delete $keygroupsbyid{$id};
next;
}
if ($id > $MAX_KEYGROUP_GID) { _err "group $name id $id is too high"; }
if ($id < $MIN_KEYGROUP_GID) { _err "group $name id $id is too low"; }
}
close($fh_group);
info "found " . (scalar keys %keygroupsbyname) . " key groups";
# checking if allowkeeper is a member of all keygroups
my @allowkeeper_groups = split(/ /, qx/groups allowkeeper/); ## no critic (ProhibitBacktickOperators)
chomp @allowkeeper_groups;
# some outputs of `groups` include "$username :" as a prefix, strip that
if ($allowkeeper_groups[0] eq 'allowkeeper' && $allowkeeper_groups[1] eq ':') {
@allowkeeper_groups = splice @allowkeeper_groups, 2;
}
foreach my $group (keys %keygroupsbyname) {
_err "allowkeeper user is not a member of group key$group" if (not grep { $_ eq "key$group" } @allowkeeper_groups);
}
# now check if each key group has a gk
# and vice versa
foreach my $group (keys %keygroupsbyname) {
next if exists $gkgroupsbyname{$group . "-gatekeeper"};
_err "key group $group is missing a gatekeeper group";
}
foreach my $groupori (keys %gkgroupsbyname) {
my $group = $groupori;
$group =~ s/-gatekeeper$//;
next if exists $keygroupsbyname{$group};
_err "gatekeeper group $group is missing a key group";
}
foreach my $group (keys %keygroupsbyname) {
next if exists $owgroupsbyname{$group . "-owner"};
_err "key group $group is missing an owner group";
}
foreach my $groupori (keys %owgroupsbyname) {
my $group = $groupori;
$group =~ s/-owner$//;
next if exists $keygroupsbyname{$group};
_err "owner group $group is missing a key group";
}
# now check if each key group has a /home/key* $HOME
# and vice versa
my @keyhomesfound;
opendir(my $dh, "/home/") or die $!;
while (my $file = readdir($dh)) {
next unless -d "/home/$file";
next if $file eq '.';
next if $file eq '..';
if ($file !~ /[a-zA-Z0-9_-]+$/) {
_err "bad chars in /home/$file";
next;
}
push @keyhomesfound, $file if $file =~ /^key/;
}
foreach my $file (@keyhomesfound) {
my $file2 = $file;
$file2 =~ s/^key//;
next if exists $keygroupsbyname{$file2};
next if (grep { $file2 eq $_ } @KEY_GROUPS_IGNORE);
_err "directory /home/key$file2 exists but no key group $file2";
}
foreach my $group (keys %keygroupsbyname) {
next if -d "/home/key$group";
_err "key group $group is missing /home/key$group";
}
my %ALL_FILES;
foreach (qx{find /home/key* /home/keykeeper /home/allowkeeper -print}) { ## no critic (ProhibitBacktickOperators)
chomp;
/$HOME_SUBDIRS_IGNORE_RE/ and next;
$ALL_FILES{$_} = 1;
}
while (my $homedir = glob '/home/*') {
-d $homedir or next;
-d "$homedir/ttyrec" or next;
next if $homedir eq '/home/proxyhttp';
next if $homedir eq '/home/healthcheck';
#$ALL_FILES{$_} = 1;
#$ALL_FILES{$_.'/ttyrec'} = 1;
#$ALL_FILES{$_.'/.ssh'} = 1;
#$ALL_FILES{$_.'/osh.log'} = 1;
my ($user) = $homedir =~ m{/([^/]+)$};
my $usertty = "$user-tty";
if (not getgrnam($usertty)) {
$usertty = substr($user, 0, 5) . '-tty';
}
check_file_rights("$homedir",
["# file: $homedir", "# owner: $user", "# group: $user", "user::rwx", "group::r-x", "group:$usertty:--x", "group:osh-auditor:--x", "mask::r-x", "other::---",],
"drwxr-x--x", $user, $user);
check_file_rights(
"$homedir/ttyrec",
[
"# file: $homedir/ttyrec", "# owner: $user", "# group: $user", "user::rwx", "group::---", "group:$usertty:r-x",
"mask::r-x", "other::---", "default:user::rwx", "default:group::---", "default:group:$usertty:r-x", "default:mask::r-x",
"default:other::---",
],
"drwxrwxr-x",
$user, $user
);
check_file_rights("$homedir/.ssh",
["# file: $homedir/.ssh", "# owner: $user", "# group: $user", "user::rwx", "group::r-x", "group:osh-auditor:--x", "mask::r-x", "other::---",],
"drwxr-x---", $user, $user);
if (-e "$homedir/osh.log") # doesn't exist? nevermind
{
check_file_rights("$homedir/osh.log", ["# file: $homedir/osh.log", "# owner: $user", "# group: $user", "user::rw-", "group::r--", "other::---",],
"-rw-r-----", $user, $user);
}
# now check all keys in ~/.ssh
opendir(my $dh, "$homedir/.ssh") or die "$homedir/.ssh: $!";
while (my $keyfile = readdir($dh)) {
next unless $keyfile =~ /^id_|private/;
my $ret = check_file_rights(
"$homedir/.ssh/$keyfile",
[
"# file: $homedir/.ssh/$keyfile",
"# owner: $user",
"# group: $user",
"user::r--",
$keyfile =~ /\.pub$/ ? "group::r--" : "group::---",
$keyfile =~ /\.pub$/ ? "other::r--" : "other::---",
],
$keyfile =~ /\.pub$/ ? "-r--r--r--" : "-r--------",
$user, $user
);
if ($keyfile !~ /\.pub$/) {
if (not $ret) {
# wow ! private key readable ?
_crit "due to above error, private key $homedir/.ssh/$keyfile might be readable !!";
}
}
else {
# check for spurious "from" in .pub
open(my $pubfh, '<', "$homedir/.ssh/$keyfile") or die "$homedir/.ssh/$keyfile: $!";
while (<$pubfh>) {
/from=/ and _err "spurious from='...' in $homedir/.ssh/$keyfile";
}
close($pubfh);
}
}
close($dh);
}
sub check_file_rights {
my $file = shift;
my $expectedOutput = shift;
my $expectedmodes = shift;
my $expectedowner = shift;
my $expectedgroup = shift;
#info "checking rights of $file";
delete $ALL_FILES{$file};
my $ok = 1;
if (not -e $file) {
_err "file $file doesn't exist!";
$ok = 0;
return $ok;
}
if (!$hasacls) {
my ($modes, $owner, $group) = (qx{ls -ld $file})[0] =~ m{(\S+)\s+\d+\s+(\S+)\s+(\S+)}; ## no critic (ProhibitBacktickOperators)
if ($modes ne $expectedmodes) { $ok = 0; _err "on $file got $modes wanted $expectedmodes"; }
if ($owner ne $expectedowner) { $ok = 0; _err "on $file got $owner wanted $expectedowner"; }
if ($group ne $expectedgroup) { $ok = 0; _err "on $file got $group wanted $expectedgroup"; }
return $ok;
}
my $param = ($islinux ? '-p' : '');
my @out = qx{getfacl $param $file 2>/dev/null}; ## no critic (ProhibitBacktickOperators)
chomp @out;
my $lineno = -1;
$expectedOutput = [sort @$expectedOutput];
@out = grep { /./ } sort @out;
foreach my $outLine (@out) {
next if not $outLine;
$lineno++;
$outLine eq $expectedOutput->[$lineno] and next;
$ok = 0;
_err "rights of $file, line$lineno, expected '" . $expectedOutput->[$lineno] . "' but got '" . $outLine . "'";
}
if (@out != @$expectedOutput) {
_err "rights of $file, number of lines unexpected (got " . @out . " instead of " . @$expectedOutput . ")";
$ok = 0;
}
return $ok;
}
# now check what is in /home/key* and the rights
foreach my $file (@keyhomesfound) {
delete $ALL_FILES{"/home/$file/.bash_logout"};
delete $ALL_FILES{"/home/$file/.bashrc"};
delete $ALL_FILES{"/home/$file/.profile"};
delete $ALL_FILES{"/home/$file/.ssh"};
delete $ALL_FILES{"/home/$file/.ssh/known_hosts"};
# check rights of /home/keytruc
if (-e "/home/$file") {
if ($file ne 'keykeeper' and $file ne 'keyreader') {
check_file_rights(
"/home/$file",
[
"# file: /home/$file", "# owner: $file", "# group: $file", "user::rwx",
"group::r-x", "group:osh-whoHasAccessTo:--x", "group:osh-auditor:--x", "group:$file-aclkeeper:--x",
"group:$file-gatekeeper:--x", "group:$file-owner:--x", "mask::r-x", "other::---",
],
"drwxr-x--x",
$file, $file
);
}
else {
check_file_rights(
"/home/$file",
[
"# file: /home/$file",
"# owner: $file",
"# group: $file",
"user::rwx",
"group::r-x",
$file eq 'keykeeper' ? "other::r-x" : "other::---", # special dir /home/keykeeper is 755
],
$file eq 'keykeeper' ? "drwxr-xr-x" : "drwxr-x---",
$file,
$file
);
}
}
else {
_err "/home/$file doesn't exist";
}
next if (grep { $file eq "key$_" } @KEY_GROUPS_IGNORE);
# check rights of /home/keytruc/allowed.ip
if (-e "/home/$file/allowed.ip") {
#not -s "/home/$file/allowed.ip" and _wrn "group $file has no servers";
check_file_rights("/home/$file/allowed.ip", ["# file: /home/$file/allowed.ip", "# owner: $file", "# group: $file-aclkeeper", "user::rw-", "group::rw-", "other::r--",],
"-rw-rw-r--", $file, "$file-aclkeeper");
}
else {
_err "/home/$file/allowed.ip doesn't exist";
}
# check rights of /home/keykeeper/keytruc/
if (-e "/home/keykeeper/$file") {
check_file_rights("/home/keykeeper/$file", ["# file: /home/keykeeper/$file", "# owner: keykeeper", "# group: $file", "user::rwx", "group::r-x", "other::r-x",],
"drwxr-xr-x", "keykeeper", $file);
}
else {
_err "/home/keykeeper/$file doesn't exist";
}
# check rights of /home/keykeeper/keytruc/id_*
opendir(my $dh, "/home/keykeeper/$file") or die "/home/keykeeper/$file: $!";
while (my $keyfile = readdir($dh)) {
next unless $keyfile =~ /^id_/; # spurious files will be reported below
my $ret = check_file_rights(
"/home/keykeeper/$file/$keyfile",
["# file: /home/keykeeper/$file/$keyfile", "# owner: keykeeper", "# group: $file", "user::r--", "group::r--", $keyfile =~ /\.pub$/ ? "other::r--" : "other::---",],
$keyfile =~ /\.pub$/ ? "-r--r--r--" : "-r--r-----",
"keykeeper", $file
);
if ($keyfile !~ /\.pub$/) {
if (not $ret) {
# wow ! private key readable ?
_crit "due to above error, private key /home/keykeeper/$file/$keyfile might be readable !!";
}
}
else {
# check for spurious "from" in .pub
open(my $pubfh, '<', "/home/keykeeper/$file/$keyfile") or die "/home/keykeeper/$file/$keyfile: $!";
while (<$pubfh>) {
/from=/ and _err "spurious from='...' in /home/keykeeper/$file/$keyfile";
}
close($pubfh);
}
}
close($dh);
}
# check some special dirs
check_file_rights("/home/allowkeeper", ["# file: /home/allowkeeper", "# owner: allowkeeper", "# group: allowkeeper", "user::rwx", "group::r-x", "other::r-x",],
"drwxr-xr-x", "allowkeeper", "allowkeeper");
check_file_rights("/home/keykeeper", ["# file: /home/keykeeper", "# owner: keykeeper", "# group: keykeeper", "user::rwx", "group::r-x", "other::r-x",],
"drwxr-xr-x", "keykeeper", "keykeeper");
check_file_rights("/home/logkeeper", ["# file: /home/logkeeper", "# owner: $UID0", "# group: bastion-users", "user::rwx", "group::-wx", "other::---",],
"drwx-wx---", $UID0, "bastion-users");
check_file_rights("/home/passkeeper", ["# file: /home/passkeeper", "# owner: $UID0", "# group: $GID0", "user::rwx", "group::r-x", "other::r-x",], "drwxr-xr-x", $UID0, $GID0);
check_file_rights("/home/oldkeeper", ["# file: /home/oldkeeper", "# owner: $UID0", "# group: $GID0", "user::rwx", "group::---", "other::---",], "drwx------", $UID0, $GID0)
if -e "/home/oldkeeper";
# now get all bastion users
my %users;
my %usersbyid;
setpwent();
while (my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $home, $shell, $expire) = getpwent()) {
if ($shell =~ /osh.pl$|diverter.sh$/) {
if (exists $users{$name}) {
_err "duplicate user $name";
}
if (exists $usersbyid{$uid}) {
_err "duplicate uid for user $name";
}
if ($home ne "/home/$name") {
_err "bad home for $name: $home";
}
if (!-d $home) {
_err "home of $name doesn't exist ($home)";
}
$users{$name} = {name => $name, uid => $uid, gid => $gid, shell => $shell};
$usersbyid{$uid} = {name => $name, uid => $uid, gid => $gid, shell => $shell};
}
# TODO check qui a un shell access
}
info "found " . (scalar keys %users) . " bastion users";
my %groups;
my %usergroups;
setgrent();
while (my ($name, $passwd, $gid, $members) = getgrent()) {
$groups{$name} = {name => $name, gid => $gid, members => [split(/ /, $members)]};
foreach my $member (split(/ /, $members)) {
push @{$usergroups{$member}}, $name;
}
}
info "found " . (scalar keys %groups) . " groups";
# check that user keyreader is a member of all bastion users primary groups
my %keyreaderuserseen;
foreach my $group (@{$usergroups{'keyreader'}}) {
$keyreaderuserseen{$group} = 1;
}
foreach my $user (keys %users) {
next if (exists $keyreaderuserseen{$user});
_err "user $user primary group doesn't have keyreader as member";
if ($ENV{'FIX_KEYREADER'}) {
system("usermod -a -G $user keyreader");
_err "... fixed!";
}
}
# check if user has /home/allowkeeper/testuser4/allowed.private
foreach my $account (keys %users) {
check_file_rights("/home/allowkeeper/$account",
["# file: /home/allowkeeper/$account", "# owner: allowkeeper", "# group: allowkeeper", "user::rwx", "group::r-x", "other::r-x",],
"drwxr-xr-x", "allowkeeper", "allowkeeper");
check_file_rights(
"/home/allowkeeper/$account/allowed.ip",
["# file: /home/allowkeeper/$account/allowed.ip", "# owner: allowkeeper", "# group: allowkeeper", "user::rw-", "group::r--", "other::r--",],
"-rw-r--r--", "allowkeeper", "allowkeeper"
);
check_file_rights(
"/home/allowkeeper/$account/allowed.private",
["# file: /home/allowkeeper/$account/allowed.private", "# owner: allowkeeper", "# group: allowkeeper", "user::rw-", "group::r--", "other::r--",],
"-rw-r--r--", "allowkeeper", "allowkeeper"
);
if (!-e "/home/allowkeeper/$account/allowed.private" && $ENV{'FIX_MISSING_PRIVATE_FILES'}) {
if (open(my $fh_priv, '>', "/home/allowkeeper/$account/allowed.private")) {
close($fh_priv);
}
chmod 0644, "/home/allowkeeper/$account/allowed.private";
my (undef, undef, $allowkeeperuid, $allowkeepergid) = getpwnam("allowkeeper");
chown $allowkeeperuid, $allowkeepergid, "/home/allowkeeper/$account/allowed.private";
_err "... fixed!";
}
# check all allowed.ip.GROUP symlinks
my $dh;
if (-d "/home/allowkeeper/$account") {
opendir($dh, "/home/allowkeeper/$account");
while (my $file = readdir($dh)) {
if ($file =~ /^config\.[a-zA-Z0-9_-]+$/) {
delete $ALL_FILES{"/home/allowkeeper/$account/$file"};
next;
}
elsif ($file !~ /^allowed\.(ip|partial)\.([a-zA-Z0-9_-]+)$/) {
next;
}
if (not grep { $2 eq $_ } keys %keygroupsbyname) {
_err "file /home/allowkeeper/$account/$file has no corresponding known group";
}
if ($1 eq 'ip') {
if (not -l "/home/allowkeeper/$account/$file") {
_err "file /home/allowkeeper/$account/$file should have been a symlink";
}
}
elsif ($1 eq 'partial') {
if (not -f "/home/allowkeeper/$account/$file") {
_err "file /home/allowkeeper/$account/$file should have been a plain file";
}
}
else {
_err "hmm, bug in the script ? got a '$1'";
}
delete $ALL_FILES{"/home/allowkeeper/$account/$file"};
}
close($dh);
}
}
delete $ALL_FILES{'/home/allowkeeper'};
delete $ALL_FILES{'/home/allowkeeper/.bash_logout'};
delete $ALL_FILES{'/home/allowkeeper/.bashrc'};
delete $ALL_FILES{'/home/allowkeeper/.profile'};
delete $ALL_FILES{'/home/allowkeeper/.ssh'};
delete $ALL_FILES{'/home/allowkeeper/activeLogin.json'};
delete $ALL_FILES{'/home/allowkeeper/expirationGrant.json'};
if (keys %ALL_FILES) {
_err "got some potentially unknown files:";
print Dumper(sort keys %ALL_FILES);
}
# for new code, check sudo stuff
sub _tocheck {
my $file = shift;
my $filesuffix = shift;
my $tocheckref = shift;
my %tocheck = %$tocheckref;
if (exists $tocheck{'NEEDGROUP'}) {
my $group = $tocheck{'NEEDGROUP'}[0];
my $gid = getgrnam($group);
if (not defined $gid) {
_err "missing group $group";
}
elsif ($gid > 1000) {
_err "group $group has a too high gid ($gid)";
}
}
my @stat = stat($file);
if (exists $tocheck{'FILEMODE'}) {
my $mode = sprintf '%04o', $stat[2] & oct(7777);
if ($mode ne $tocheck{'FILEMODE'}[0]) {
_err "bad file mode on $file, got $mode but expected " . $tocheck{'FILEMODE'}[0];
}
}
if (exists $tocheck{'FILEOWN'}) {
my $uid = $stat[4];
my $gid = $stat[5];
my $wantuser = (split / /, $tocheck{'FILEOWN'}[0])[0];
my $wantgroup = (split / /, $tocheck{'FILEOWN'}[0])[1];
$wantuser = $UID0 if $wantuser eq 'root';
$wantgroup = $GID0 if $wantgroup eq 'root';
if ($uid ne getpwnam($wantuser)) {
_err "bad owner on file $file (got $uid but wanted $wantuser)";
}
if ($gid ne getgrnam($wantgroup)) {
_err "bad group on file $file (got $gid but wanted $wantgroup)";
}
}
if (exists $tocheck{'SUDOERS'}) {
my $sudoersfile = "$sudoers_dir/osh-plugin-" . $filesuffix;
if (not -f $sudoersfile) {
_err "sudoers file $sudoersfile doesn't exists";
}
else {
my $mode = sprintf '%04o', (stat($sudoersfile))[2] & oct(7777);
if ($mode ne "0440") {
_err "sudoers file $sudoersfile has a bad mode $mode";
}
if (!open(my $fh_sudoers, '<', $sudoersfile)) {
_err "can't open sudoers file $sudoersfile to check";
}
else {
my @contents = <$fh_sudoers>;
close($fh_sudoers);
chomp @contents;
foreach my $wantedline (@{$tocheck{'SUDOERS'}}) {
if (not grep { $_ eq $wantedline } @contents) {
_err "missing line in plugin $sudoersfile: $wantedline";
}
}
}
}
}
if (exists $tocheck{'KEYSUDOERS'}) {
my @contents;
foreach my $sudoersfile (sort <$BASEDIR/etc/sudoers.group.template.d/*>) {
if (!open(my $fh_sudoers, '<', $sudoersfile)) {
_err "can't open sudoers file template $sudoersfile to check";
}
else {
my @lines = <$fh_sudoers>;
close($fh_sudoers);
chomp @lines;
push @contents, @lines;
}
}
if (@contents) {
foreach my $wantedline (@{$tocheck{'KEYSUDOERS'}}) {
$wantedline =~ s'@KEYGROUP@'%GROUP%'g;
if (not grep { $_ eq $wantedline } @contents) {
_err "missing line in plugin sudoers.group.template: $wantedline";
}
}
}
}
foreach my $key (qw{ FILEMODE FILEOWN SUDOERS NEEDGROUP KEYSUDOERS }) {
delete $tocheck{$key};
}
if (keys %tocheck) {
_err "hum sparse tocheck key: " . join(" ", sort keys %tocheck);
}
return 1;
}
while (my $file = glob "$BASEDIR/bin/helper/*") {
my ($filesuffix) = $file =~ m{/osh-([a-zA-Z0-9_-]+$)};
if (!$filesuffix) {
_err "helper file has a strange name ($file)";
next;
}
my $fh_helper;
if (!open($fh_helper, '<', $file)) {
_err "can't open helper file $file to check";
next;
}
my %tochecklocal;
while (<$fh_helper>) {
/^#/ or last;
if (/^\s*#\s*$/) {
_tocheck($file, $filesuffix, \%tochecklocal);
%tochecklocal = ();
next;
}
/^# ([A-Z0-9]+) (.+)$/ or next;
my ($keyword, $line) = ($1, $2);
push @{$tochecklocal{$keyword}}, $line;
}
close($fh_helper);
if (%tochecklocal) {
_tocheck($file, $filesuffix, \%tochecklocal);
}
}
# check /etc/sudoers.d vs $BASEDIR/etc/sudoers.d
# FIXME won't see if we have too many / old files in /etc/sudoers.d
while (my $distfile = glob "$BASEDIR/etc/sudoers.d/*") {
my $prodfile = $distfile;
$prodfile =~ s=^\Q$BASEDIR\E/etc/sudoers.d=$sudoers_dir=;
if (-e $prodfile) {
my @md5sums;
foreach my $file ($prodfile, $distfile) {
if (open(my $fh, '<', $file)) {
binmode($fh);
push @md5sums, Digest::MD5->new->addfile($fh)->hexdigest;
close($fh);
}
else {
push @md5sums, "ERR($file)";
}
}
if ($md5sums[0] ne $md5sums[1]) {
_err "sudoers file $distfile and $prodfile differ";
}
}
else {
_err "sudoers file $prodfile not found";
}
}
if (1) {
my @template;
foreach my $sudoersfile (sort <$BASEDIR/etc/sudoers.group.template.d/*>) {
if (!open(my $fh_sudoers, '<', $sudoersfile)) {
_err "can't open sudoers file template $sudoersfile to check";
}
else {
my @lines = <$fh_sudoers>;
close($fh_sudoers);
chomp @lines;
push @template, @lines;
}
}
my %seensudogroupfile;
while (my $sudoersfile = glob "$sudoers_dir/osh-group-*") {
# TODO check 0440
# TODO check there's a matching group (and the other way around)
my $group = $sudoersfile;
$group =~ s/^.*osh-group-key//;
$seensudogroupfile{$group} = 1;
my $fh_sudoers;
if (!open($fh_sudoers, '<', $sudoersfile)) {
_err "can't open $sudoersfile file to check: $!";
next;
}
my @contents = <$fh_sudoers>;
close($fh_sudoers);
chomp @contents;
my @expected = @template;
do { s/%GROUP%/key$group/g; s=%BASEPATH%=/opt/bastion=g; }
for @expected;
foreach (@expected) {
my $wantedline = $_; # copy
if (not grep { $_ eq $wantedline } @contents) {
_err "missing line in $sudoersfile: $wantedline";
}
}
}
foreach my $group (keys %keygroupsbyname) {
next if exists $seensudogroupfile{$group};
_err "missing $sudoers_dir/osh-group-key$group file";
}
}
exit($bad > 255 ? 255 : $bad);

545
bin/admin/check-ssh-hardening.pl Executable file
View file

@ -0,0 +1,545 @@
#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;
use Term::ANSIColor;
use IPC::Open2;
use MIME::Base64;
use Getopt::Long;
use File::Temp qw{ tempfile };
my $hideok = 0;
sub ko ## no critic (RequireArgUnpacking)
{
print colored("[ERR!] " . $_[0] . "\n", "red");
return 1;
}
sub ok ## no critic (RequireArgUnpacking)
{
$hideok and return 1;
print colored("[ ok ] " . $_[0] . "\n", "green");
return 1;
}
sub wrn ## no critic (RequireArgUnpacking)
{
print colored("[warn] " . $_[0] . "\n", "yellow");
return 1;
}
sub inf ## no critic (RequireArgUnpacking)
{
print colored("[info] " . $_[0] . "\n", "blue");
return 1;
}
my $generate_moduli;
GetOptions(
'hide-ok' => \$hideok,
'generate-moduli=i', \$generate_moduli
);
my (%h, %d);
# %h contains the sshd configuration for this host
# %d contains the default sshd configuration of this sshd version
my $fh_cmd;
open($fh_cmd, '-|', '/usr/sbin/sshd -T 2>/dev/null') or die($!);
while (<$fh_cmd>) {
/^(\S+)\s+(.+)$/ and push @{$h{$1}}, $2;
}
if (not keys %h) {
# newer openssh versions need some context to give their config
open($fh_cmd, '-|', '/usr/sbin/sshd -T -C user=root -C host=localhost -C addr=localhost 2>/dev/null') or die($!);
while (<$fh_cmd>) {
/^(\S+)\s+(.+)$/ and push @{$h{$1}}, $2;
}
}
close($fh_cmd);
open($fh_cmd, '-|', "/usr/sbin/sshd -T -f /dev/null 2>/dev/null") or die($!);
while (<$fh_cmd>) {
/^(\S+)\s+(.+)$/ and push @{$d{$1}}, $2;
}
close($fh_cmd);
# hacky way to find out ciphers/kex/macs on old sshd versions
if (not $d{ciphers} or not $d{kexalgorithms} or not $d{macs}) {
# hacky way
if (!open($fh_cmd, '-|', "strings /usr/sbin/sshd")) {
ko "Error trying to get the ciphers/kexs/macs list ($!)";
}
else {
my ($ciphers, $kexalgorithms, $macs);
while (<$fh_cmd>) {
/arcfour128,/ and $ciphers = $_;
/mac-sha1,/ and $macs = $_;
/diffie-hellman.*,.*diffie-hellman/ and $kexalgorithms = $_;
}
close($fh_cmd);
chomp($ciphers, $macs, $kexalgorithms);
$d{ciphers} or $d{ciphers}[0] = $ciphers;
$h{ciphers} or $h{ciphers}[0] = $ciphers;
$d{macs} or $d{macs}[0] = $macs;
$h{macs} or $h{macs}[0] = $macs;
$d{kexalgorithms} or $d{kexalgorithms}[0] = $kexalgorithms;
$h{kexalgorithms} or $h{kexalgorithms}[0] = $kexalgorithms;
}
}
my @myciphers = split /,/, $h{ciphers}[0];
my %ciphers = (
"3des-cbc" => 1,
"blowfish-cbc" => 1,
"cast128-cbc" => 1,
"arcfour" => 1,
"arcfour128" => 1,
"arcfour256" => 1,
"aes128-cbc" => 2,
"aes192-cbc" => 2,
"aes256-cbc" => 2,
"rijndael-cbc\@lysator.liu.se" => 2,
"aes128-ctr" => 3,
"aes192-ctr" => 3,
"aes256-ctr" => 3,
"aes128-gcm\@openssh.com" => 3,
"aes256-gcm\@openssh.com" => 3,
"chacha20-poly1305\@openssh.com" => 3,
);
my %list;
foreach my $cipher (split /,/, $d{ciphers}[0]) {
if ($ciphers{$cipher} == 1) {
push @{$list{((grep { $cipher eq $_ } @myciphers) ? 'weakon' : 'weakoff')}}, $cipher;
}
elsif ($ciphers{$cipher} == 2) {
push @{$list{((grep { $cipher eq $_ } @myciphers) ? 'mediumon' : 'mediumoff')}}, $cipher;
}
elsif ($ciphers{$cipher} == 3) {
push @{$list{((grep { $cipher eq $_ } @myciphers) ? 'highon' : 'highoff')}}, $cipher;
}
else { push @{$list{'unknown'}}, $cipher }
}
$list{'weakon'} and wrn "ciphers: found enabled weak ciphers " . join(',', @{$list{'weakon'}});
$list{'weakoff'} and ok "ciphers: found disabled weak ciphers " . join(',', @{$list{'weakoff'}});
$list{'mediumon'} and ok "ciphers: found enabled medium-grade ciphers " . join(',', @{$list{'mediumon'}});
$list{'mediumoff'} and ok "ciphers: found disabled medium-grade ciphers " . join(',', @{$list{'mediumoff'}});
$list{'highon'} and ok "ciphers: found enabled high-grade ciphers " . join(',', @{$list{'highon'}});
$list{'highoff'} and wrn "ciphers: found disabled high-grade ciphers " . join(',', @{$list{'highoff'}});
my @mymacs = split /,/, $h{macs}[0];
my %macs = (
"hmac-sha1" => 1,
"hmac-sha1-96" => 1,
"hmac-sha2-256" => 2,
"hmac-sha2-512" => 2,
"hmac-md5" => 1,
"hmac-md5-96" => 1,
"hmac-ripemd160" => 1,
"hmac-ripemd160\@openssh.com" => 1,
"umac-64\@openssh.com" => 2,
"umac-128\@openssh.com" => 2,
"hmac-sha1-etm\@openssh.com" => 1,
"hmac-sha1-96-etm\@openssh.com" => 1,
"hmac-sha2-256-etm\@openssh.com" => 3,
"hmac-sha2-512-etm\@openssh.com" => 3,
"hmac-md5-etm\@openssh.com" => 1,
"hmac-md5-96-etm\@openssh.com" => 1,
"hmac-ripemd160-etm\@openssh.com" => 2,
"umac-64-etm\@openssh.com" => 2,
"umac-128-etm\@openssh.com" => 2,
"hmac-sha2-256-96" => 2,
"hmac-sha2-512-96" => 2
);
%list = ();
foreach my $mac (split /,/, $d{macs}[0]) {
if (not exists $macs{$mac}) {
wrn "Unknown mac $mac";
next;
}
if ($macs{$mac} == 1) {
push @{$list{((grep { $mac eq $_ } @mymacs) ? 'weakon' : 'weakoff')}}, $mac;
}
elsif ($macs{$mac} == 2) {
push @{$list{((grep { $mac eq $_ } @mymacs) ? 'mediumon' : 'mediumoff')}}, $mac;
}
elsif ($macs{$mac} == 3) {
push @{$list{((grep { $mac eq $_ } @mymacs) ? 'highon' : 'highoff')}}, $mac;
}
else { push @{$list{'unknown'}}, $mac }
}
$list{'weakon'} and wrn "macs: found enabled weak MACs " . join(',', @{$list{'weakon'}});
$list{'weakoff'} and ok "macs: found disabled weak MACs " . join(',', @{$list{'weakoff'}});
$list{'mediumon'} and ok "macs: found enabled medium-grade MACs " . join(',', @{$list{'mediumon'}});
$list{'mediumoff'} and ok "macs: found disabled medium-grade MACs " . join(',', @{$list{'mediumoff'}});
$list{'highon'} and ok "macs: found enabled high-grade MACs " . join(',', @{$list{'highon'}});
$list{'highoff'} and wrn "macs: found disabled high-grade MACs " . join(',', @{$list{'highoff'}});
my @mykexs = split /,/, $h{kexalgorithms}[0];
my %kexs = (
"diffie-hellman-group1-sha1" => 1,
"diffie-hellman-group14-sha1" => 1,
"diffie-hellman-group-exchange-sha1" => 1,
"diffie-hellman-group-exchange-sha256" => 3,
"ecdh-sha2-nistp256" => 2,
"ecdh-sha2-nistp384" => 2,
"ecdh-sha2-nistp521" => 2,
"curve25519-sha256\@libssh.org" => 3,
"curve25519-sha256" => 3,
"diffie-hellman-group16-sha512" => 3,
"diffie-hellman-group18-sha512" => 3,
"diffie-hellman-group14-sha256" => 3,
);
%list = ();
foreach my $kex (split /,/, $d{kexalgorithms}[0]) {
if (not exists $kexs{$kex}) {
wrn "Unknown kex $kex";
next;
}
if ($kexs{$kex} == 1) {
push @{$list{((grep { $kex eq $_ } @mykexs) ? 'weakon' : 'weakoff')}}, $kex;
}
elsif ($kexs{$kex} == 2) {
push @{$list{((grep { $kex eq $_ } @mykexs) ? 'mediumon' : 'mediumoff')}}, $kex;
}
elsif ($kexs{$kex} == 3) {
push @{$list{((grep { $kex eq $_ } @mykexs) ? 'highon' : 'highoff')}}, $kex;
}
else { push @{$list{'unknown'}}, $kex }
}
$list{'weakon'} and wrn "kexs: found enabled weak KEXs " . join(',', @{$list{'weakon'}});
$list{'weakoff'} and ok "kexs: found disabled weak KEXs " . join(',', @{$list{'weakoff'}});
$list{'mediumon'} and ok "kexs: found enabled medium-grade KEXs " . join(',', @{$list{'mediumon'}});
$list{'mediumoff'} and ok "kexs: found disabled medium-grade KEXs " . join(',', @{$list{'mediumoff'}});
$list{'highon'} and ok "kexs: found enabled high-grade KEXs " . join(',', @{$list{'highon'}});
$list{'highoff'} and wrn "kexs: found disabled high-grade KEXs " . join(',', @{$list{'highoff'}});
my $hasecdsa = 0;
my $hased25519 = 0;
my $hasrsa = 0;
foreach my $file (@{$h{hostkey}}) {
if (not -e $file) {
ko "hostkey: $file defined in config but not found on disk!";
next;
}
if (!open($fh_cmd, '-|', "ssh-keygen -lf $file.pub")) {
ko "hostkey: $file.pub can't be opened for verification!";
next;
}
my $out = <$fh_cmd>;
close($fh_cmd);
chomp $out;
if (not $out =~ m{^(\d+) .+ \((.+)\)$}) {
ko "hostkey: $file can't be parsed ($out)";
next;
}
my ($size, $algo) = ($1, $2); ## no critic (ProhibitCaptureWithoutTest)
if ($algo eq 'DSA') { ko "hostkey: DSA $size host key found, you should get rid of it" }
elsif ($algo eq 'RSA') {
$size >= 4096 and ok "hostkey: RSA $size host key found";
$size < 4096 and ko "hostkey: RSA $size host key found, this is too small (< 4096)";
$hasrsa = 1;
}
elsif ($algo eq 'ECDSA') {
ok "hostkey: ECDSA $size host key found";
$hasecdsa = 1;
}
elsif ($algo eq 'ED25519') {
ok "hostkey: Ed25519 $size host key found";
$hased25519 = 1;
}
else {
ko "hostkey: Unknown host key found ($file: $out)";
}
}
if (!$hasecdsa) {
if (grep { /_ecdsa_/ } @{$d{'hostkey'}}) {
ok "hostkey: You don't have any ECDSA key, maybe you don't like NIST curves, that's your right!";
}
else {
ok "hostkey: You don't have any ECDSA key (but it's not supported by your SSH)";
}
}
if (!$hased25519) {
if (grep { /_ed25519_/ } @{$d{'hostkey'}}) {
wrn "hostkey: You don't have any Ed25519 key, generate one!";
}
else {
ok "hostkey: You don't have any Ed25519 key (but it's not supported by your SSH)";
}
}
$hasrsa || wrn "hostkey: You don't have any RSA key, generate one!";
# loading known moduli
my $delimiterseen = 0;
my @xz;
my %knownmoduli;
my %foundmoduli;
open(my $fh_myself, '<', $0) or die $!;
while (<$fh_myself>) {
chomp;
$delimiterseen and push @xz, $_;
$delimiterseen++ if ($_ eq '__MODULI__');
}
close($fh_myself);
my $decoded = decode_base64(join("\n", @xz));
my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'unxz', '-c'); #TODO get rid of this call
print CHLD_IN $decoded;
close(CHLD_IN);
my $rawlist;
while (<CHLD_OUT>) {
$rawlist .= $_;
}
waitpid($pid, 0);
my $child_exit_status = $? >> 8;
if ($child_exit_status != 0) {
ko "moduli: Error getting list of well known moduli";
}
else {
foreach (split /\n/, $rawlist) {
chomp;
$knownmoduli{$_} = 1;
}
}
# now moduli stuff
if (!open(my $fh_moduli, '<', "/etc/ssh/moduli")) {
ko "Couldn't open /etc/ssh/moduli to check it ($!)";
}
else {
my %moduli;
my $atleast8191 = 0;
while (<$fh_moduli>) {
chomp;
/^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/ or next; ## no critic (ProhibitUnusedCapture)
push @{$moduli{$5}}, $1;
$foundmoduli{$1} = 1;
}
close($fh_moduli);
foreach my $size (sort keys %moduli) {
my $count = scalar @{$moduli{$size}};
my $nbknown = 0;
foreach my $mod (@{$moduli{$size}}) {
$nbknown++ if exists $knownmoduli{$mod};
}
if ($size < 2047) { ko "moduli: found $count weak moduli of size $size ($nbknown well-known)" }
elsif ($size < 4095) { wrn "moduli: found $count medium moduli of size $size ($nbknown well-known)" }
else { ok "moduli: found $count strong moduli of size $size ($nbknown well-known)" }
$size >= 8191 and $atleast8191++;
}
if (not $atleast8191) {
wrn "moduli: found no moduli of size of at least 8191";
}
my $wellknown = 0;
foreach my $mod (sort keys %foundmoduli) {
exists $knownmoduli{$mod} and $wellknown++;
}
if ($wellknown == 0) {
ok "moduli: None of your moduli is well-known (searched for " . (scalar keys %knownmoduli) . " well-known moduli), nice!";
}
else {
my $nbmod = scalar keys %foundmoduli;
wrn "moduli: Found $wellknown/$nbmod well-known moduli in your file ("
. ($wellknown * 100.0 / $nbmod)
. "%), looked for "
. (scalar keys %knownmoduli)
. " well-known moduli";
}
}
sub check_config_value {
my $key = shift;
my $default = shift;
my $expected = shift;
my $current_value = $default;
if (exists $h{lc($key)}) {
$current_value = $h{lc($key)}[0];
}
else {
if (open(my $fh_config, '<', '/etc/ssh/sshd_config')) {
while (<$fh_config>) {
chomp;
/^\Q$key \E(.+)$/i or next;
$current_value = $1;
ok "config(debug): parsed from conf $key as '$current_value'";
last;
}
close($fh_config);
}
}
ref $expected ne 'ARRAY' and $expected = [$expected];
if (grep { $current_value eq $_ } @$expected) {
ok "config: $key is set to '$current_value'";
}
else {
wrn "config: $key is set to '$current_value', expected one of: " . join(',', @$expected);
}
return 1;
}
check_config_value 'UsePAM', 'no', [qw{ yes 1 }];
check_config_value 'LoginGraceTime', 120, [(1 .. 120)];
check_config_value 'MaxAuthTries', 6, [(1 .. 15)];
check_config_value 'IgnoreRHosts', 'no', 'yes';
check_config_value 'StrictModes', 'yes', 'yes';
check_config_value 'PermitRootLogin', 'yes', [qw{ no without-password forbid-password }];
check_config_value 'PermitEmptyPasswords', 'no', 'no';
check_config_value 'UsePrivilegeSeparation', 'yes', [qw{ yes sandbox }];
check_config_value 'PermitTunnel', 'yes', [qw{ 0 no }];
check_config_value 'AllowAgentForwarding', 'yes', 'no';
check_config_value 'AllowTcpForwarding', 'yes', 'no';
# check passwords
foreach (qx{passwd -Sa}) ## no critic (ProhibitBacktickOperators)
{
/^(\S+)\s+(\S+)/ or next;
my ($login, $status) = ($1, $2);
if ($status eq "P") {
wrn "passwd: account $login has a usable password! maybe run usermod -L $login";
}
elsif ($status eq "NP") {
wrn "passwd: account $login has an empty password!!! set one or run usermod -L $login";
}
elsif ($status ne "L") {
wrn "passwd: account $login has a weird passwd status ($status)";
}
elsif ($login eq 'root') {
ok "password: account $login has a locked password";
}
}
# get a list of valid shells
my %shells;
if (open(my $fh_shells, '<', '/etc/shells')) {
while (<$fh_shells>) {
chomp;
/^#/ and next;
$shells{$_} = 1;
}
close($fh_shells);
}
# then check for ssh keys on valid shells
if (open(my $fh_passwd, '<', '/etc/passwd')) {
while (<$fh_passwd>) {
chomp;
my @tokens = split /:/;
my $shell = $tokens[6];
next unless exists $shells{$shell};
my $login = $tokens[0];
# has a valid shell
my $home = $tokens[5];
foreach my $file ("$home/.ssh/authorized_keys", "$home/.ssh/authorized_keys2") {
next unless -e $file;
if (open(my $fh_auth, '<', $file)) {
while (<$fh_auth>) {
chomp;
/^\s*#/ and next;
/^\s*$/ and next;
my $short = $_;
length($short) > 99 and $short = substr($short, 0, 45) . '...' . substr($short, length($short) - 45);
inf "sshkey: login $login has a shell ($shell) and a key: $short";
}
close($fh_auth);
}
}
}
close($fh_passwd);
}
# check umask
my $umaskFound = undef;
if (open(my $fh_login, '<', '/etc/login.defs')) {
while (<$fh_login>) {
/^UMASK\s+(.+)/ or next;
if ($1 ne '027' or not defined $umaskFound) {
$umaskFound = $1;
}
}
close($fh_login);
if (not $umaskFound) {
wrn "umask: no value found, expected 027 in /etc/login.defs";
}
elsif ($umaskFound ne '027') {
wrn "umask: bad value found ($umaskFound), need 027 in /etc/login.defs";
}
else {
ok "umask: expected 027 value found";
}
}
if (open(my $fh_pam, '<', '/etc/pam.d/common-session')) {
my $umaskOk = 0;
while (<$fh_pam>) {
/^\s*session\s+optional\s+pam_umask\.so\s+umask=0?027/ or next;
ok "umask: correct umask found in pam.d";
$umaskOk = 1;
last;
}
close($fh_pam);
if (not $umaskOk) {
wrn "umask: no pam.d umask configuration found or bad one";
}
}
if (defined $generate_moduli and $generate_moduli > 0) {
my ($fh, $file_unchecked) = tempfile("moduli.unchecked.$generate_moduli.XXXXXX", SUFFIX => '.txt', TMPDIR => 1);
local $SIG{'INT'} = sub { unlink($file_unchecked); };
print "Generating candidates of size $generate_moduli...\n";
system("nice ssh-keygen -G $file_unchecked -b $generate_moduli");
print "Validating generated candidates of size $generate_moduli...\n";
system("nice ssh-keygen -T /tmp/moduli.checked.$generate_moduli.pid$$.txt -f $file_unchecked");
unlink($file_unchecked);
}
__END__
__MODULI__
/Td6WFoAAATm1rRGAgAhARYAAAB0L+Wj4H38EaRdAAUJiSlag5YALZsrn4vX1kL+swvtsDNqbhi5jgRqer9uFoOL/l1RVa2n1UisIBkstmyQX2e0I3/ERtnaY09bixqcdtyodOdXMaBU4xn+59EBJhAKyNi8IYwFkLXs92s4o3
VGs0BSb5HhIv+9KorGOzj/SgZG35nSVlpby5g+GErLTzBQlY4tX9Rfn3Sdvd0U6e3rhHAJuEU9npV7+/rynSZ+8Raob0IgD1DOs39p0S+BLvNF0iwo4cYokP4TJ7/ZiVYApfpuZsDmPQh1IW2gG1aw76Jg7NiJb2GTP5DpZkm+
1PzfVeF+sgB4IIMFplEp87/YEVFVYoutQ4WL7QSsFxZKr6UWkEJ02UE87wc2V/MEmkbFDDQi0qfRdZep7FmdE7DAsqHjUuKQxICnSDfwNvm7ZKwbUvdQZTdOZaTsrK++jRdRUYtCyp67HQ7rkQslbvdC/4E2unplRBFAvFSj6I
Z503HfCO6x0K+akz39ptUmSfaVwM3mjIpQ82qGtL/atu87hB0mT0MwpIkrW8BRwZwV5H21wEfz2A3tSDsQ3/n5OlGtH91yso2IYxHLC0ggd2mCSnjB+u4pUDNRpHxMUqUgv9pyyYAm3OTXT3zDu38EKBXt01WBHUPiLLRgRRb5
1FdpCWMptRV2zrdXJ8e2nugOba5LHdOWHHbvUBkGo8P0a4D8OTw7C9Vag/Ezvp+zW2W5y6B0PLi43UspJT1zU+BxZDoohV0ySdX6AQBZnfmEem84IfUB0m8VFUxplhbMoUPYxWueUH5Eoe3bt4yLFSspdYBxXGLmlyi5v6rORa
5NBmoXoUtUxHgPn/+p6Y2DHzULDB/MBnaLNBM1OJ9h1aftUeYq6SD9+KuaMaocv9EbfESOPj3AuEPX9afUdlNXgeJY5nmAQ0+rneIeB0xjR/lD5+ReUZTuZQgn/NO2a5glz7XRCE/HET082/sOFuFmBDbBkgZz/jKSrhbKJfZp
WAi7Q+GdjHpmiQ1fRmnr0dcuOs1uJ0DqR+fuxjQLWbXerx1qvtJdGwF2cIOpUQfXZrI0I5ZSXosUZoh0roGb7EG1kse4Pu6PU1Q8dZBsCX77keX/aiGnoamyKpwyWaF6VTBZIlNtbHzXNHNd36u2qHtfM5Fg+Vr6z7Y3Kz50E8
H1ALKRjrHX4zHP+AS0KdguYLVTW1urIgFbrd/34e+3k4PY7Kr7A3DFjjCx/T3vAfiB63wGzo8QJ3aEDIfEX6A+XcMEvfxx/qdjJCZoT5/b6phCtIQCxJkxU/6ZcTs3yrRkKskZZO4JE7iB5dZwiPXznB0Zmoow96r7zKQSL4va
6ahcMXHyPMpD0MP/n4rnMm7qxLcrS/TFSMoS4uNalS3HLlmMv40brBlnpZcfbk+iuW8P2xervK8WlzI9Xi43Xy00iZDC/pwPC7pGiGqePawE6AhK46XXWbj/Tujz+wRDw3OqdvTd1sO0grnQd4Rx8dUbgQ9aQk8b2jjTyd2Hhk
/qUVuTjoCwvLq60ZFjPjN4Z5S/TGwbddkOnMOgqRwYUdiQyj2G9HJZjakO3/uW6Ud5VTMbOIH5VYnb4iQCaw/3IpknDrvkWdb3Lj8eibgUUNzYglLrmr6udvhAWw5CQbMhYDgqFVkElnQv04Qji+2NhSsuUMhDxzMkmfvqjNDs
TiSX33KZZC9wgd15yTw68hhcApuxZrdkuwjmaINGgs92T1hE/0NW5ZafpCyijtdWBY7O8fhURGQbxIUBVu718Z9EjXigX1kuPXVmqHspiyJo5T8/o02Q9eoQTeNZIcLHwZediHS0dt0lrZLouDKx2RcWihAoxX99F9xiJ35i6C
EmncZVrHnXDCnWDJPyVRUI4cmYlGcgITGHFOaK9gtoo/IxfmCTCXsreuz+mXjMqlOSMvMYeprFsKiVFdq105HdLMXb2kpyXIj5hWAefggV59EVCcbMJgY8Nh9sOlzRvKoGEfj+9ZdiyqOduxoIAoGUOC52K2v7eIhiG5Z19qiT
QmXmDbPPOVYJcuxUbeyQIBxrHCOukEVxkPuCyffAjEf2oYkyHpH21ngk+roKkOhQJWiGwwUDUxXZp5R7iVhsPk5u0uMIzTrGugRwrHEZGeIKIGwvJ5GbyYTraI0qNYPaK5llz/MpFHdlqAtXG2qfL4tbCr/trrOFgQAC9y117v
8pzVOygwl4wmQVBCMMyI99mGTtnbkwRwRhA4t4GwP+cKXMo4+smRVvAlxVWAV++wCCZTfSu/FQdviVDxAbNPUQoEvAl8KSGWszSDWxnrffwSRafMRA3W3GAJt8ExXpp3jJmYqCINCB3vzX4/LWL6ypsuHPd63mgPS0L2sIR/zE
ChMtv9kTCh/Q/9hk8egcpQX8UG2WaBm+BE7UeuY0nid0y9sUxlPlKJcl2iGbMMOPIyGABZ0OzWXk17ta1CeVCAjXByIkbeoIwwrT/6XzVo4bodrM5iLAMNOiMDBznQj2I/UcWfvRVHraXjPG/b+NQAslEUyZdoSB78U5yv2NMG
eXIdlQ3eeJNJAfHAG4G5wdjJ0qNzdMDyaYhXfWgvkj7A2lYtKDdPwZChm+Q2EblxPN7DR9jUNhw7JhXUNa5ASCTdw0cOzvV1FYyT832us3/FYktRSGUbT+5nbIB+IZA82trUj7Awui6bg1ew0JKPlHsFeDugY6GLQrhtgE3ZDX
XoPcDXEPjTlJ2eR94k2ala0coe61I+0OfQ/Xl9ocicDpSXE97GUqqA/QfyCbDNv/hRd+75Nk+FW1Gkpi2iuy2/vR6BL0daxmAi428JQscKBsEGSjvPn11kmLp0UnHiEPkaTRm4GrrV+07tfOaZnIlKxs0MUFnI4dhJdB2xX0hi
b9FAFMzsP9BiBp6ZwEbjsstX0W3VCeGS3OeVaWlP7DULGE7agS9d9HkKZuw5mS2fmvO0c9HNpGrVoqDp5xfcggLVW744NDMPAkuRWIx1t6Exz1rzpDfZV0MN9PZf5gCg/TzOZcTtagwaITWCM9/J1hrnNueH5WbStDo4DwGpqD
LuWQoQzRdk5mfmzFUHbidczooLsjqiYRK9fwwztT+A0la/yYvMobR4vLoENgyNSCVF1Ei4bPXwL+VawqN6WYK5rK090gyhsDsVgzgNYbkV4urRb2+cDeoHN7o3nvUcj99Cozqv8zjD/30M+x34t/l6jpfrvy/7IJczOOCK82Qu
XvA97fxvLgBmtL1q7KPrb5LackAyRfItPtxZ1aM/vHWtHqsSI+l0BwdsqBeJe6cGWib6jWCEj2CWPC3D+X3fkte1qhHHSvHGFprNq15hRUp5MSYkNpI4OYrRj5hBbYSnTrYizbrIIssfrnF6ynEhGzr12pJCxAbK0PVfvaUkN1
NmMZfgdsk5Zf/nVhsT3UT3mWewNHqAWqG5yQizXhSNOGMAzzVjP/Xy1Uz1t9Al4BPc+LS80/6Q9KGokMx9DS02jqNWuwTJUVqJaoNcbvL8UREzGB8Ndt88QlBvKZdqqn1s9aUSA6e0SQnwwR05KeniCz7HJf2sPo06WrHMt2p9
tm/CAobg3vCP3ZimViSe68KxUM6LqXir/pCAcCklCoJEqhLKzLH/lrEE7IdWlbhgXVf4dENehFNzLwe05yxKX+jWvkEWG0z9C9zsgOTUjxixtoOnpszpgnayyTI3tcSOsPWZJHU88Nx5GM1VHxtFF93EvBJza90hZath/DhhRw
h6hZ8OWtmtIlWVGi/6oerhBF3yJxKB6VCaWyqHyTbiA722ADq+h3/ul99A57Rk1vzN0/neDJb0YWrzk1WofrFY+J44NtO7cArHLd2UKdbbLR1jMYax0wvu5gkdlJh2FCg5oJne0ZRQm+y8ScWyqk4dJbmw152MScHpqVFdrt7d
qWjusb94MRfyqV5ppqb3A5KJ4cdXPs+k30aAxzyMVmZbSGHL3TbwcduxI/aY3UNOxTXE5+Co1m78XdzmDTTg+gi1Udmv9VNl2+r4rn8pbghw6wcZlWyMSeZYKflfqu8jF5kRM0mq3tgF02bmmb8FzsXEC5okJi/iJkuQFzK/y9
y4mGUa1AowA4p2wBtq4xH/Dv0r+yirirSAFSJGppGC5CVxlG4vg+3+M1lutSNunBLfjXPplFdpdzad6lbDuQbBVXK80km8m29OXYt27FF76o3kOjkdb7adbbKZzK3eY8CSGuBZjN6X0DMBM5KcJQOo4XtNeQhZzd3px4V0RqmB
+NyMaC9EcAdFEJZ6K8QJ0S7HSXOfVRMS41TSXkz/L1cPTuRgbb/y/F+ona91ag3u6dNH2Mpw0FQMYg6hrtR8pd2lv0zaWbWNUffl/krQvdzENGKsW6zRsO7z0OM9ZikfQEnEo0RNj0Jn8r4oqWaf1e+BgvIxmSG08JtDZjo0f4
SM7gB/0oTGYzCysqxmdJ6vnv5kbVtm+KszveBB77PNDcj1MGeVG38LM1Hl/h4HkGt+1zDy87lc8jRbA6gcvYqKHv9ls651aV9d6qg23+K4rGgH0mCeEhCySLC06n+/hSwzmU8tOhpp8nSy3lBa6CeHnDYRyKSxPMtVdZD/rS1o
YCVr2BAZU5s2GY0AZgiAhprEpQqkfPmSiMXthV8DXmOb4P10T62GJfqgjsDbjg5LoYS4sl4OvsJ3LC8bCAo2nsqTGrb4CE+zbmn9L3MAnNYKHAnnhK/CZILBaCDalt1pSWiogkEOrtWjNZ/mX/OCDWAF1/kkMDS0trrzlNDQwn
LTLwmkkWBpqzzIiE5UJcMQA35+/gjbvQBjG3t3K5Q48ee44pAYcaVFCm6sCvzjZl5GXpQZv9XCNqXf+PjuEIsnCUodA8tmvV9nY3LyTmLDM2XZ8SmEQ/NbwLbpfM1l25mFLLTbfIXWO7WVEb7gtuHGmqPijGpgZh/Ubhc91+Lp
EgbEGRyJJKsUoPf/cie49oYurfwWwBB3qppPwaCtyRHLKIgJHJZXtf6M97ZpQW69DjbDgileth/6il6GbBxK/vrdQ52McwmLpnW1IhsymO0wq2OLt0tWxBVODaQPDtOKt/P49rKir8DL+3sM0XnjvTiI4XENwxi7qavLqaSNnB
4irzcrI+fEI4RSnZRAsGaPiRlLxism1JsDSzhgoatfXYKVYZvzXFHXpos+uXdTAW4Rb1ymu/TOKDwCKgUTm6i/4RQowPr5Xt4aOgAZS1TGqDCSguOYZDN/dQiVpFhDO8mA0esB5YITcE8ATXzMx40D8wMbJ28HVABotdWYHlY2
2/nmlQq4LeGoFXQHoZD4osxXyqOR46R+IbmjbndwmJl5XSZMJmUSboWdGKIs0D1E+cbtxYBHKLasupXOeEGmSxCF4iYGQjSmHT060e1otJDgv9/QA+imEOA9qiSLd1N3ZPQGeCt8WwV9Qqs90+1y9c3gtJRgaM6pEA7Se3sYzS
gTRKR6SDFN/uQo8MxATwGrC5chJLGH80TZj2v2F4I96Y2Xf20B5HKTNXCExmxw4xQEjZfqhrulblZuipuGD/lRCPAyNUEPMOLdD2veLeWIRLOD+N8i1gaC7jubmmLjkLYKbNKlpBhynwPfGzn2OL76zGXQRksFgdSNAhXmp5A6
o/rimulgO4pbCJ1Dkheu/fjpIUAZfryy+umwDoXwgkrES5++a5YLz4FBzVw9avP7T0ykrK5Bw/Ld1MoXM+rkp5JfHMFhTbicKndVKk5GeJ3WbPhjM1yaP+X/ac0nkQ1oWYXBjmoCbGgFw6O3Zhv5PL7+gtetsCWif4AQkLxQFo
5OoTvtDspWc7IBpQEEAp81St2VbgfSMzGVCWUi+LC/INMBk0z45hjiDqPZXRCJfwdFODahXjDCPkYuHfBaUOlvkwHzZ6pftxJ7tmBB7cYLWhu/3cC39o3eAd3G3xUGoeF8dODsS8yNrX4PS4Vk6kzuvTvgY/KgIAC5Y+IC2P1Q
/8RDaF4VCznj3IG4AiFZgsJv+4UbLzHiYCjqPfHyNxZY3p7E77JknAYXAJXAf/LHQQBsff4rbuYgCScaJ7wLC4zSnVam4rekpBOTH+QS5+LFgqOAAAOphsNU8vQF4AAcAj/fsBALf8WoCxxGf7AgAAAAAEWVo=

128
bin/admin/fix-group-gid.sh Executable file
View file

@ -0,0 +1,128 @@
#! /usr/bin/env bash
# vim: set filetype=sh ts=4 sw=4 sts=4 et:
set -e
basedir=$(readlink -f "$(dirname "$0")"/../..)
# shellcheck source=lib/shell/functions.inc
. "$basedir"/lib/shell/functions.inc
MINGID=10000
if [ -n "$2" ] || [ -z "$1" ] ; then
echo "Usage: $0 <groupname|ALL>"
exit 2
fi
fail()
{
echo "Error, will not proceed: $*"
exit 1
}
really_run_commands=0
something_to_do=0
_run()
{
something_to_do=1
if [ "$really_run_commands" = "1" ] ; then
echo "Executing: $*"
read -r ___
"$@"
else
echo "DRY RUN: would execute: $*"
fi
}
find_next_available_gid()
{
nextgid=$((MINGID + 1))
while getent group "$nextgid" >/dev/null; do
nextgid=$((nextgid + 1))
done
echo $nextgid
}
change_gid()
{
group="$1"
type="$2"
maingroup=$(echo "$group" | sed -re 's/-(aclkeeper|gatekeeper|owner)//g')
if [ "$type" != secondary ]; then
getent passwd "$group" >/dev/null || fail "user $group doesn't exist"
fi
if [ "$type" != secondary ]; then
getent group "$group" >/dev/null || fail "group $group doesn't exist"
else
getent group "$group" >/dev/null || return
fi
oldgid=$(getent group "$group" | awk -F: '{print $3}')
[ "$oldgid" -ge "$MINGID" ] && return
newgid=$(find_next_available_gid)
_run group_change_gid_compat "$group" "$newgid"
tocheck=""
for dir in "/home/$group" "/home/keykeeper/$group" "/home/$maingroup" "/home/keykeeper/$maingroup"; do
test -d "$dir" && tocheck="$tocheck $dir"
done
if [ -n "$tocheck" ]; then
# shellcheck disable=SC2086
_run find $tocheck -gid "$oldgid" -exec chgrp "$group" '{}' \;
fi
if command -v getfacl >/dev/null && command -v setfacl >/dev/null; then
( cd / ; _run sh -c "getfacl /home/$maingroup 2>/dev/null | sed -re 's/:$oldgid:/:$group:/' | setfacl --restore=-" )
fi
}
batchrun()
{
something_to_do=0
change_gid "key$from"
change_gid "key$from-gatekeeper" secondary
change_gid "key$from-aclkeeper" secondary
change_gid "key$from-owner" secondary
}
main()
{
from=$(echo "$from" | sed -re 's/^key//')
if [ "$from" = "keeper" ] || [ "$from" = "reader" ]; then
echo "$from: special group, skipping."
return
fi
really_run_commands=0
batchrun
if [ "$something_to_do" = 0 ]; then
echo "$from: nothing to do."
return
fi
echo
echo "$group: OK to proceed ? (CTRL+C to abort). You'll still have to validate each commands I'm going to run"
# shellcheck disable=SC2034
read -r ___
really_run_commands=1
batchrun
echo "$group: done."
}
if [ "$1" = "ALL" ]; then
groups=$(getent group | grep "^key" | cut -d: -f1 | grep -Ev -- '-(aclkeeper|gatekeeper|owner)$')
for from in $groups
do
main
done
else
from="$1"
main
fi

63
bin/admin/fixrights.sh Executable file
View file

@ -0,0 +1,63 @@
#! /usr/bin/env bash
# vim: set filetype=sh ts=4 sw=4 sts=4 et:
set -e
basedir=$(readlink -f "$(dirname "$0")"/../..)
# shellcheck source=lib/shell/functions.inc
. "$basedir"/lib/shell/functions.inc
action_doing "Adjusting rights on $basedir"
if [ ! -w "$basedir" ]; then
action_error "$basedir is not writable"
exit 1
fi
# we must ensure that all basedir parents are at least o+x
parent="$basedir"
while [ -n "$parent" ];
do
chmod o+x "$parent"
parent=$(echo "$parent" | sed -re 's=/+[^/]+$==')
done
find "$basedir" -name .git -prune -o -print0 | xargs -r0 chown "$UID0:$GID0"
chmod o+x "$basedir"
find "$basedir" -name .git -prune -o -type d -print0 | xargs -r0 chmod 0755
find "$basedir" -name .git -prune -o -name contrib -prune -o -type f -print0 | xargs -r0 chmod 0644
find "$basedir"/bin/ ! -name "*.json" -print0 | xargs -r0 chmod 0755
chmod 0644 "$basedir"/bin/dev/perlcriticrc
chmod 0700 "$basedir"/bin/admin/install
chmod 0700 "$basedir"/contrib
chmod 0700 "$basedir"/bin/sudogen
while IFS= read -r -d '' file
do
filemode=$(awk '/# FILEMODE / { print $3; exit; }' "$file")
fileown=$(awk '/# FILEOWN / { print $3":"$4; exit; }' "$file")
if [ -z "$filemode" ] && [ -z "$fileown" ]; then
action_error "Missing info for $file"
else
action_detail "$filemode $fileown $file"
chmod -- "$filemode" "$file"
chown -- "$fileown" "$file"
fi
done < <(find "$basedir/bin/helper" -type f -print0)
chmod 0755 "$basedir"/docker/entrypoint.sh \
"$basedir"/tests/functional/docker/docker_build_and_run_tests.sh \
"$basedir"/tests/functional/docker/docker_build_and_run_tests_all.sh \
"$basedir"/tests/functional/launch_tests_on_instance.sh \
"$basedir"/tests/functional/docker/target_role.sh \
"$basedir"/tests/functional/docker/tester_role.sh \
"$basedir"/tests/functional/fake_ttyrec.sh \
"$basedir"/tests/unit/run.pl
while IFS= read -r -d '' plugin
do
groupname=$(basename "$plugin")
getent group "osh-$groupname" >/dev/null || continue
chown "$UID0:osh-$groupname" "$plugin"
chmod 0750 "$plugin"
done < <(find "$basedir/bin/plugin/restricted/" ! -name "*.json" -print0)
action_done ""

View file

@ -0,0 +1,60 @@
#! /usr/bin/env bash
# vim: set filetype=sh ts=4 sw=4 sts=4 et:
set -e
basedir=$(readlink -f "$(dirname "$0")"/../..)
# shellcheck source=lib/shell/functions.inc
. "$basedir"/lib/shell/functions.inc
account="$1"
if [ -z "$account" ] ; then
echo "Usage: $0 ACCOUNT" >&2
exit 1
fi
action_doing "Granting all restricted commands to $account"
if ! getent passwd "$account" >/dev/null ; then
action_error "Account $account not found"
exit 2
fi
if ! getent passwd "$account" | grep -q /osh.pl$ ; then
action_error "Account $account doesn't seem to be a bastion account"
exit 4
fi
if ! cd "$basedir"/bin/plugin/restricted; then
action_error "Error trying to access the restricted plugins directory"
exit 3
fi
allok=1
for group in auditor $(ls)
do
echo "$group" | grep -Fq . && continue
group="osh-$group"
if getent group "$group" >/dev/null ; then
if getent group "$group" | grep -qE ":$account$|:$account,|,$account,|,$account$" ; then
action_detail "Account was already in group $group"
else
if add_user_to_group_compat "$account" "$group" ; then
action_detail "Account added to group $group"
else
action_error "Error adding user... continuing anyway"
allok=0
fi
fi
else
action_error "group $group doesn't exist, ignoring"
allok=0
fi
done
if [ "$allok" = 1 ] ; then
action_done "$account has been granted to all restricted commands"
exit 0
else
action_warn "Got some errors adding $account to all restricted commands"
exit 1
fi

1294
bin/admin/install Executable file

File diff suppressed because it is too large Load diff

135
bin/admin/osh-sync-watcher.sh Executable file
View file

@ -0,0 +1,135 @@
#! /usr/bin/env bash
# vim: set filetype=sh ts=4 sw=4 sts=4 et:
PIDFILE=/var/run/osh-sync-watcher.pid
basedir=$(readlink -f "$(dirname "$0")"/../..)
# shellcheck source=lib/shell/functions.inc
. "$basedir"/lib/shell/functions.inc
configfile="$BASTION_ETC_DIR/osh-sync-watcher.sh"
if [ ! -e "$configfile" ] ; then
# to allow for smooth upgrades, look for the old file name if new is not found
configfile="$BASTION_ETC_DIR/sync-watcher.sh"
if [ ! -e "$configfile" ] ; then
echo "No configuration found, exiting"
exit 0
fi
fi
rsyncfilterfile="$BASTION_ETC_DIR/osh-sync-watcher.rsyncfilter"
if [ ! -e "$rsyncfilterfile" ] ; then
# to allow for smooth upgrades, look for the old file name if new is not found
rsyncfilterfile="$BASTION_ETC_DIR/sync-watcher-rsync.filter"
if [ ! -e "$rsyncfilterfile" ] ; then
echo "No rsync filter file found, exiting"
exit 0
fi
fi
# load configuration
# shellcheck source=etc/bastion/osh-sync-watcher.sh.dist
. "$configfile"
# if a logdir is defined, tail to the log
# shellcheck disable=SC2154
if [ -n "$logdir" ]; then
[ ! -d "$logdir" ] && mkdir -p "$logdir"
exec &>> >(tee -a "$logdir/osh-sync-watcher.log")
fi
# if a syslog facility is defined, set the proper variable
# so that _log _warn and _err do log to syslog,
# also don't talk on stdout
if [ -n "$syslog" ]; then
LOG_FACILITY="$syslog"
LOG_QUIET=1
fi
if [ "$enabled" != "1" ] ; then
_log "Script is not enabled (review the config in $configfile if needed)"
exit 0
fi
# is another copy of myself still running ?
if [ -e "$PIDFILE" ] ; then
oldpid=$(head -1 "$PIDFILE")
if kill -0 -- "$oldpid" ; then
_log "Another copy of myself is running ($oldpid), exiting"
exit 0
else
_log "Another copy of myself apparently died ($oldpid), cleaning up"
fi
fi
# shellcheck disable=SC2064
trap "rm -f $PIDFILE" EXIT
rm -f "$PIDFILE"
# race condition here ... but /var/run is writable only by root
echo "$$" > "$PIDFILE"
while :
do
_log "Watching for changes (timeout: $timeout)..."
# we'll cap to the max allowed
maxfiles=$(test -r /proc/sys/fs/inotify/max_user_watches && cat /proc/sys/fs/inotify/max_user_watches || echo 4096)
{
# account/group creation/deletion:
echo /etc/passwd
echo /etc/group
echo /home/allowkeeper
echo /home/keykeeper
echo /home/passkeeper
# all allowed.ip files of bastion groups:
for grouphome in $(getent group | grep -Eo '^key[a-zA-Z0-9_-]+' | grep -Ev -- '-(aclkeeper|gatekeeper|owner)$' | sed 's=^=/home/='); do
test -e "$grouphome/allowed.ip" && echo "$grouphome/allowed.ip"
done
# all authorized_keys files of bastion accounts:
for accountssh in $(getent passwd | grep ":$basedir/bin/shell/osh.pl\$" | cut -d: -f1 | sed 's=^=/home/=;s=$=/.ssh/='); do
find "$accountssh" -mindepth 1 -maxdepth 1 -name 'authorized_keys*' ! -name "*.backup*" -type f -print
done
} | head -"$maxfiles" | timeout "$timeout" inotifywait -e close_write -e moved_to -e create -e delete -e delete_self --quiet --recursive --csv --fromfile - ; ret=$?
if [ "$ret" = 124 ] ; then
_log "... timed out, syncing just in case!"
elif [ "$ret" = 0 ] ; then
_log "... got event, syncing in 3 secs!"
sleep 3
else
_warn "... got weird return value $? (maxfiles=$maxfiles); sleeping a bit..."
sleep "$timeout"
fi
# sanity check myself before
if [ ! -d /home/allowkeeper ] || ! [ -d /home/keykeeper ] || ! [ -d /home/logkeeper ] || \
[ "$(find /home -mindepth 2 -maxdepth 2 -type f -name lastlog 2>/dev/null | wc -l)" = 0 ] ; then
_log "Own sanity check failed (maybe I'm locked?), not syncing and sleeping"
sleep "$timeout"
continue
fi
# /sanity
_log "Starting sync!"
# shellcheck disable=SC2154
[ -z "$remotehostlist" ] && remotehostlist="$remotehost"
for remote in $remotehostlist
do
if echo "$remote" | grep -q ':'; then
remoteport=$(echo "$remote" | cut -d: -f2)
remote=$(echo "$remote" | cut -d: -f1)
else
remoteport=22
fi
if [ -e "$LOCKFILE" ] && [ $(( $(date +%s) - $(stat -c %Y "$LOCKFILE") )) -le 300 ]; then
_log "$remote: [1/3] syncing needed data postponed for next run (upgrade lockfile present)"
else
_log "$remote: [1/3] syncing needed data..."
rsync -vaA --numeric-ids --delete --filter "merge $rsyncfilterfile" --rsh "$rshcmd -p $remoteport" / "$remoteuser@$remote:/"
_log "$remote: [1/3] sync ended with return value $?"
fi
_log "$remote: [2/3] syncing lastlog files from master to slave, only if master version is newer..."
rsync -vaA --numeric-ids --update --include '/' --include '/home/' --include '/home/*/' --include '/home/*/lastlog' --exclude='*' --rsh "$rshcmd -p $remoteport" / "$remoteuser@$remote:/"
_log "$remote: [2/3] sync ended with return value $?"
_log "$remote: [3/3] syncing lastlog files from slave to master, only if slave version is newer..."
find /home -mindepth 2 -maxdepth 2 -type f -name lastlog | rsync -vaA --numeric-ids --update --prune-empty-dirs --include='/' --include='/home' --include='/home/*/' --include-from=- --exclude='*' --rsh "$rshcmd -p $remoteport" "$remoteuser@$remote:/" /
_log "$remote: [3/3] sync ended with return value $?"
done
done

146
bin/admin/packages-check.sh Executable file
View file

@ -0,0 +1,146 @@
#! /usr/bin/env bash
# vim: set filetype=sh ts=4 sw=4 sts=4 et:
set -e
basedir=$(readlink -f "$(dirname "$0")"/../..)
# shellcheck source=lib/shell/functions.inc
. "$basedir"/lib/shell/functions.inc
opt_dev=0
opt_install=0
opt_syslogng=0
opt_ttyrec=0
opt_supervisor=0
while builtin getopts "distv" opt; do
# shellcheck disable=SC2154
case "$opt" in
"d") opt_dev=1;;
"i") opt_install=1;;
"s") opt_syslogng=1;;
"t") opt_ttyrec=1;;
"v") opt_supervisor=1;;
*) echo "Error $opt"; exit 1;;
esac
done
action_doing "Detecting OS..."
action_detail "Found $OS_FAMILY"
if [ "$OS_FAMILY" = Linux ]; then
action_detail "Found distro $LINUX_DISTRO version $DISTRO_VERSION (major $DISTRO_VERSION_MAJOR), distro like $DISTRO_LIKE"
fi
action_done
action_doing "Checking the list of installed packages..."
if echo "$DISTRO_LIKE" | grep -q -w debian; then
wanted_list="libcommon-sense-perl libjson-perl libnet-netmask-perl libnet-ip-perl \
libnet-dns-perl libdbd-sqlite3-perl libterm-readkey-perl libdatetime-perl \
fortunes-bofh-excuses sudo fping \
xz-utils sqlite3 binutils acl libtimedate-perl libgnupg-perl gnupg rsync \
libjson-xs-perl inotify-tools lsof curl libterm-readline-gnu-perl \
libwww-perl libdigest-sha-perl libnet-ssleay-perl \
libnet-server-perl cryptsetup mosh expect openssh-server locales \
coreutils netcat bash libcgi-pm-perl iputils-ping"
[ "$opt_dev" = 1 ] && wanted_list="$wanted_list libperl-critic-perl perltidy"
if { [ "$LINUX_DISTRO" = debian ] && [ "$DISTRO_VERSION_MAJOR" -lt 9 ]; } ||
{ [ "$LINUX_DISTRO" = ubuntu ] && [ "$DISTRO_VERSION_MAJOR" -le 16 ]; }; then
wanted_list="$wanted_list openssh-blacklist openssh-blacklist-extra"
fi
if { [ "$LINUX_DISTRO" = debian ] && [ "$DISTRO_VERSION_MAJOR" -ge 8 ]; } ||
{ [ "$LINUX_DISTRO" = ubuntu ] && [ "$DISTRO_VERSION_MAJOR" -ge 14 ]; }; then
wanted_list="$wanted_list liblinux-prctl-perl libpam-google-authenticator pamtester"
fi
[ "$opt_syslogng" = 1 ] && wanted_list="$wanted_list syslog-ng syslog-ng-core"
[ "$opt_ttyrec" = 1 ] && wanted_list="$wanted_list ovh-ttyrec"
[ "$opt_supervisor" = 1 ] && wanted_list="$wanted_list supervisor"
if [ "$opt_install" = 1 ]; then
export DEBIAN_FRONTEND=noninteractive
# shellcheck disable=SC2086
apt-get update && apt-get install -y $wanted_list
exit $?
fi
installed=$(dpkg -l | awk '/^ii/ {print $2}' | cut -d: -f1)
install_cmd="apt-get install"
elif echo "$DISTRO_LIKE" | grep -q -w rhel; then
wanted_list="perl-JSON perl-Net-Netmask perl-Net-IP \
perl-Net-DNS perl-DBD-SQLite perl-TermReadKey perl-DateTime \
sudo fping xz sqlite binutils acl perl-TimeDate gnupg rsync \
perl-JSON-XS inotify-tools lsof curl perl-Term-ReadLine-Gnu \
perl-libwww-perl perl-Digest perl-Net-Server cryptsetup mosh \
expect openssh-server nc bash perl-CGI perl(Test::More) passwd \
cracklib-dicts perl-Time-Piece perl-Time-HiRes which \
perl-Sys-Syslog pamtester google-authenticator"
if [ "$DISTRO_VERSION_MAJOR" = 7 ]; then
wanted_list="$wanted_list fortune-mod coreutils"
fi
[ "$opt_syslogng" = 1 ] && wanted_list="$wanted_list syslog-ng"
[ "$opt_ttyrec" = 1 ] && wanted_list="$wanted_list ovh-ttyrec"
[ "$opt_supervisor" = 1 ] && wanted_list="$wanted_list supervisor"
if [ "$opt_install" = 1 ]; then
if [ "$DISTRO_VERSION_MAJOR" = 8 ]; then
sed -i -e 's/enabled=.*/enabled=1/g' /etc/yum.repos.d/CentOS-PowerTools.repo
sed -i -e 's/enabled=.*/enabled=1/g' /etc/yum.repos.d/CentOS-Extras.repo
fi
yum install -y epel-release
# shellcheck disable=SC2086
yum install -y $wanted_list
exit 0
fi
installed="FIXME"
install_cmd="yum install"
elif echo "$DISTRO_LIKE" | grep -q -w suse; then
wanted_list="perl-common-sense perl-JSON perl-Net-Netmask perl-Net-IP \
perl-Net-DNS perl-DBD-SQLite perl-TermReadKey perl-DateTime \
fortune sudo fping \
xz sqlite binutils acl perl-TimeDate gnupg rsync \
perl-JSON-XS inotify-tools lsof curl perl-TermReadLine-Gnu \
perl-libwww-perl perl-Digest perl-IO-Socket-SSL \
perl-Net-Server cryptsetup mosh expect openssh \
coreutils netcat-openbsd bash perl-CGI iputils \
perl-Time-HiRes which perl-Unix-Syslog hostname"
wanted_list="$wanted_list google-authenticator-libpam"
# perl-GnuPG
[ "$opt_syslogng" = 1 ] && wanted_list="$wanted_list syslog-ng"
[ "$opt_ttyrec" = 1 ] && wanted_list="$wanted_list ovh-ttyrec"
[ "$opt_supervisor" = 1 ] && wanted_list="$wanted_list python-supervisor python-setuptools"
if [ "$opt_install" = 1 ]; then
if [ "$opt_supervisor" = 1 ]; then
zypper addrepo https://download.opensuse.org/repositories/home:bmanojlovic/openSUSE_Leap_15.0/home:bmanojlovic.repo
zypper refresh
fi
# shellcheck disable=SC2086
zypper install -y $wanted_list
exit $?
fi
installed="FIXME"
install_cmd="zypper install"
elif [ "$OS_FAMILY" = FreeBSD ]; then
if [ "$opt_install" = 1 ]; then
pkg install -y rsync bash sudo p5-JSON p5-JSON-XS p5-common-sense p5-Net-IP p5-GnuPG p5-DBD-SQLite p5-Net-Netmask p5-Term-ReadKey expect fping p5-Net-Server p5-CGI p5-LWP-Protocol-https
exit $?
fi
else
echo "This script doesn't support this OS yet ($DISTRO_LIKE)" >&2
exit 1
fi
missing=''
for i in $wanted_list ; do
ok=0
for j in $installed ; do
[ "$i" = "$j" ] && ok=1 && break
done
[ $ok = 1 ] || missing="$missing $i"
done
if [ -n "$missing" ] ; then
action_error "Some packages are missing, to install them, use:"
action_detail "$install_cmd$missing"
else
action_done "All needed packages are installed"
fi

110
bin/admin/rename-group.sh Executable file
View file

@ -0,0 +1,110 @@
#! /usr/bin/env bash
# vim: set filetype=sh ts=4 sw=4 sts=4 et:
from=$1
to=$2
if [ -n "$3" ] || [ -z "$2" ] ; then
echo "Usage: $0 original_group_name new_group_name"
exit 2
fi
fail()
{
echo "Error, will not proceed: $*"
exit 1
}
really_run_commands=0
_run()
{
if [ "$really_run_commands" = "1" ] ; then
echo "Executing: $*"
read -r ___
"$@"
else
echo "DRY RUN: would execute: $*"
fi
}
batchrun()
{
getent group "key$from" >/dev/null || fail "group key$from doesn't exist"
getent group "key$to" >/dev/null && fail "group key$to already exists"
_run groupmod -n "key$to" "key$from"
getent passwd "key$from" >/dev/null || fail "user key$from doesn't exist"
getent passwd "key$to" >/dev/null && fail "user key$to already exists"
_run usermod -l "key$to" "key$from"
if getent group "key$from-gatekeeper" >/dev/null ; then
# key$from-gatekeeper might not exist if the group name was too long from the beginning
getent group "key$to-gatekeeper" >/dev/null && fail "group key$to-gatekeeper already exists"
_run groupmod -n "key$to-gatekeeper" "key$from-gatekeeper"
fi
if getent group "key$from-owner" >/dev/null ; then
# key$from-owner sometimes doesn't exist for old groups, so nevermind
getent group "key$to-owner" >/dev/null && fail "group key$to-owner already exists"
_run groupmod -n "key$to-owner" "key$from-owner"
fi
test -d "/home/key$from" || fail "directory /home/key$from doesn't exists"
test -d "/home/key$to" && fail "directory /home/key$to already exists"
_run mv -v "/home/key$from" "/home/key$to"
test -d "/home/keykeeper/key$from" || fail "directory /home/keykeeper/key$from doesn't exists"
test -d "/home/keykeeper/key$to" && fail "directory /home/keykeeper/key$to already exists"
_run mv -v "/home/keykeeper/key$from" "/home/keykeeper/key$to"
if test -e "/etc/sudoers.d/osh-group-key$from" ; then
# if exists, will move it
test -e "/etc/sudoers.d/osh-group-key$to" && fail "file /etc/sudoers.d/osh-group-key$to already exists"
_run mv -v "/etc/sudoers.d/osh-group-key$from" "/etc/sudoers.d/osh-group-key$to"
_run sed -i -re "s/key$from/key$to/g" "/etc/sudoers.d/osh-group-key$to"
fi
keykeeper="/home/keykeeper/key$from"
[ "$really_run_commands" = "1" ] && keykeeper="/home/keykeeper/key$to"
# shellcheck disable=SC2044
for key in $(find "$keykeeper"/ -type f -name "id_*$from*" ! -name "*.pub")
do
test -e "$key" || continue
test -e "$key.pub" || fail "file $key.pub doesn't exist"
keyto=$(echo "$key" | sed -re "s/(id_.*)$from/\\1$to/")
test -e "$keyto" && fail "file $keyto already exists"
test -e "$keyto.pub" && fail "file $keyto.pub already exists"
_run mv -v "$key" "$keyto"
_run mv -v "$key.pub" "$keyto.pub"
done
for account in /home/allowkeeper/*/
do
fromfile="$account/allowed.partial.$from"
tofile="$account/allowed.partial.$to"
test -e "$fromfile" || continue
test -e "$tofile" && fail "file $tofile already exists"
_run mv -v "$fromfile" "$tofile"
done
for account in /home/allowkeeper/*/
do
fromfile="$account/allowed.ip.$from"
tofile="$account/allowed.ip.$to"
test -L "$fromfile" || continue
test -e "$tofile" && fail "file $tofile already exists"
_run rm -vf "$fromfile"
_run ln -vs "/home/key$to/allowed.ip" "$tofile"
done
}
really_run_commands=0
batchrun
echo
echo "OK to proceed ? (CTRL+C to abort). You'll still have to validate each commands I'm going to run"
# shellcheck disable=SC2034
read -r ___
really_run_commands=1
batchrun
echo "Done."

61
bin/admin/restore-account.sh Executable file
View file

@ -0,0 +1,61 @@
#! /usr/bin/env bash
# vim: set filetype=sh ts=4 sw=4 sts=4 et:
account="$1"
backup_path="$2"
if [ -z "$backup_path" ] || [ -n "$3" ]; then
echo "Restores a deleted account's data."
echo "The account must have been re-created first."
echo "WARNING: the newly created account information will be overwritten (keys, accesses)"
echo
echo "Usage: $0 <account> <backup_path>"
echo "Example: $0 johndoe /home/oldkeeper/accounts/johndoe.at-1502153197.by-admin"
exit 1
fi
if ! getent passwd "$account" >/dev/null ; then
echo "Account '$account' doesn't seem to exist, you must re-create it first"
exit 2
fi
homedir=$(getent passwd "$account" | cut -d: -f6)
if [ -z "$homedir" ] || ! [ -d "$homedir" ]; then
echo "Account '$account's homedir doesn't seem to exist ($homedir)"
exit 2
fi
if [ ! -d "$backup_path" ]; then
echo "Backup path '$backup_path' doesn't exist or is not a folder!"
exit 2
fi
if [ ! -d "$backup_path/allowkeeper" ] || ! [ -d "$backup_path/$account-home" ] ; then
echo "Backup path '$backup_path' doesn't seem to be a valid backup path!"
exit 2
fi
echo "Here is the contents of the allowkeeper dir of $account:"
find "/home/allowkeeper/$account/"
echo "Here is the contents of the current homedir of $account:"
find "$homedir/"
echo
echo -n "This will be replaced, does this look reasonable (y/n) ? "
read -r ans
if [ "$ans" != "y" ]; then
echo "Aborting."
exit 3
fi
chattr -a "$homedir"/*.log
mkdir "$homedir"/before-restore
chmod 0 "$homedir"/before-restore
find "$homedir" -mindepth 1 -maxdepth 1 ! -name before-restore -print0 | xargs -r0 mv -v -t "$homedir"/before-restore
rsync -vaP "$backup_path/$account-home/" "$homedir/"
chown -R "$account:$account" "$homedir/"
chattr +a "$homedir"/*.log
rsync -vaP --delete "$backup_path"/allowkeeper/ "/home/allowkeeper/$account/"
echo "New allowkeeper info is as follows:"
ls -l "/home/allowkeeper/$account/"
echo
echo "Done."

205
bin/admin/setup-encryption.sh Executable file
View file

@ -0,0 +1,205 @@
#! /usr/bin/env bash
# vim: set filetype=sh ts=4 sw=4 sts=4 et:
set -e
umask 077
basedir=$(readlink -f "$(dirname "$0")"/../..)
# shellcheck source=lib/shell/functions.inc
. "$basedir"/lib/shell/functions.inc
action_doing "Checking whether the proper tools are installed"
if ! command -v rsync >/dev/null || ! command -v cryptsetup >/dev/null; then
action_error "Missing rsync or cryptsetup, aborting"
exit 1
else
action_done
fi
action_doing "Checking whether /home is a separate partition"
home_block_device=$(awk '/ \/home / {print $1}' /proc/mounts)
if [ -n "$home_block_device" ] && [ -e "$home_block_device" ]; then
action_done "found $home_block_device"
else
action_error "No, aborting"
exit 1
fi
action_doing "Checking whether /home is in /etc/fstab"
if grep -qE '[[:space:]]/home[[:space:]]' /etc/fstab; then
action_done "$(grep '[[:space:]]/home[[:space:]]' /etc/fstab)"
else
action_error "No, aborting"
exit 1
fi
action_doing "Checking whether we can umount /home"
if umount /home; then
action_done
else
action_error "No, aborting"
exit 1
fi
action_doing "Checking whether we can remount /home"
if mount /home; then
action_done
else
action_error "No, aborting"
exit 1
fi
action_doing "Checking used space in /home"
home_used_mb=$(df -m /home | awk '{ print $3 }' | tail -n1)
if [ -n "$home_used_mb" ]; then
action_done "$home_used_mb MiB"
else
action_error "Couldn't get the /home used space"
exit 1
fi
action_doing "Checking available space in /"
slash_available_mb=$(df -m / | awk '{ print $4 }' | tail -n1)
if [ -n "$slash_available_mb" ]; then
action_done "$slash_available_mb MiB"
else
action_error "Couldn't get the / available space"
exit 1
fi
action_doing "Checking whether there is enough available space in / to hold /home contents temporarily"
if [ "$slash_available_mb" -gt "$home_used_mb" ]; then
action_done
else
action_error "Not enough free space in /"
exit 1
fi
action_doing "Creating temporary /tmphome"
# silently try to delete it just in case it exists but is empty
if [ -d /tmphome ]; then
rmdir /tmphome 2>/dev/null || true
fi
if [ -e /tmphome ]; then
action_error "/tmphome already exists! Aborting"
exit 1
else
mkdir /tmphome
if [ ! -d /tmphome ]; then
action_error "Couldn't create /tmphome!"
exit 1
else
action_done
fi
fi
action_doing "Rsyncing /home to /tmphome"
if rsync -vaPHAX --exclude='lost+found' /home/ /tmphome/; then
action_done
else
action_error "Rsync failed, aborting!"
rm -Rf /tmphome
exit 1
fi
action_doing "Rsync done, here are some details:"
action_detail "ls /home : $(cd /home ; find . | tr '\n' ' ')"
action_detail "ls /tmphome: $(cd /tmphome ; find . | tr '\n' ' ')"
action_detail "du -shc /home : $(du -shc /home | grep total)"
action_detail "du -shc /tmphome: $(du -shc /tmphome | grep total)"
action_detail ""
action_detail "Does this look reasonable? [CTRL+C if not]"
# shellcheck disable=SC2034
read -r _dummy
action_doing "Umounting /home"
if umount /home; then
action_done
else
action_error "Couldn't umount /home, aborting"
rm -Rf /tmphome
exit 1
fi
action_doing "Erasing /home block device and encrypting it (last chance to cancel!)"
action_detail "You should generate a strong password on your desk, with e.g. \`pwgen -s 10\`"
if cryptsetup luksFormat "$home_block_device"; then
action_done
else
action_error "Cryptsetup failed, aborting"
mount /home && rm -Rf /tmphome
exit 1
fi
action_doing "Opening newly encrypted block device"
if cryptsetup luksOpen "$home_block_device" home; then
action_done
else
action_error "Opening failed, aborting! Your /home partition is no longer valid, fix it manually! ($home_block_device)"
exit 1
fi
action_doing "Creating a new filesystem on top of the encrypted block device"
if mkfs.ext4 -T news -L home -M /home /dev/disk/by-id/dm-name-home; then
action_done
else
action_error "Filesystem creation failed, aborting! Your /home partition is no longer valid, fix it manually! ($home_block_device)"
exit 1
fi
action_doing "Setting up /etc/bastion/luks-config.sh with encrypted block device"
if sed -i -re "s;^DEV_ENCRYPTED=.*;DEV_ENCRYPTED=$home_block_device;" /etc/bastion/luks-config.sh; then
action_done
else
action_error "Couldn't modify /etc/bastion/luks-config.sh, please do it manually, continuing"
fi
action_doing "Setting up /etc/fstab with encrypted block device"
newfstab=$(mktemp)
grep -Ev "[[:space:]]/home[[:space:]]" /etc/fstab > "$newfstab"
echo "# added by $(basename "$0") on $(date)" >> "$newfstab"
echo "/dev/disk/by-id/dm-name-home /home ext4 defaults,errors=remount-ro,noauto,nosuid,noexec,nodev 0 0" >> "$newfstab"
cat "$newfstab" > /etc/fstab
rm -f "$newfstab"
action_done
action_doing "Remounting /home after encryption"
if mount /home; then
action_done
else
action_error "Error while remounting home, aborting!"
exit 1
fi
action_doing "Rsyncing back /home contents"
if rsync -vaPHAX --remove-source-files --exclude='lost+found' /tmphome/ /home/; then
action_done
else
action_error "Rsync failed, aborting!"
exit 1
fi
action_doing "Removing /tmphome"
if find /tmphome -depth -type d -empty -delete; then
action_done
else
action_error "Error while removing /tmphome, continuing anyway"
fi
action_doing "Testing whether we can properly unlock /home after boot"
if umount /home; then
if cryptsetup luksClose home; then
if /opt/bastion/bin/admin/unlock-home.sh; then
action_done
else
action_error "Error with unlock-home.sh, ignoring"
fi
else
action_error "Couldn't luksClose home, ignoring"
fi
else
action_error "Couldn't umount /home to run the test, ignoring"
fi
[ ! -e /root/unlock-home.sh ] && ln -s /opt/bastion/bin/admin/unlock-home.sh /root/

View file

@ -0,0 +1,25 @@
#! /usr/bin/env bash
# vim: set filetype=sh ts=4 sw=4 sts=4 et:
set -e
basedir=$(readlink -f "$(dirname "$0")"/../..)
# shellcheck source=lib/shell/functions.inc
. "$basedir"/lib/shell/functions.inc
if [ -z "$2" ] || [ -n "$3" ]; then
echo "Usage: $0 <NAME> <UID>"
echo "Note: UID can be the special value 'AUTO'"
exit 1
fi
if [ "$2" = AUTO ] || [ "$2" = auto ]; then
USER=root HOME=/root "$basedir/bin/plugin/restricted/accountCreate" '' '' '' '' --uid-auto --account "$1"
else
USER=root HOME=/root "$basedir/bin/plugin/restricted/accountCreate" '' '' '' '' --uid "$2" --account "$1"
fi
"$basedir"/bin/admin/grant-all-restricted-commands-to.sh "$1"
add_user_to_group_compat "$1" "osh-admin"
sed_compat 's/^"adminAccounts": \[\]/"adminAccounts": ["'"$1"'"]/' "$BASTION_ETC_DIR/bastion.conf"

141
bin/admin/setup-gpg.sh Executable file
View file

@ -0,0 +1,141 @@
#! /usr/bin/env bash
# vim: set filetype=sh ts=4 sw=4 sts=4 et:
set -e
umask 077
basedir=$(readlink -f "$(dirname "$0")"/../..)
# shellcheck source=lib/shell/functions.inc
. "$basedir"/lib/shell/functions.inc
if command -v gpg1 >/dev/null 2>&1; then
gpgcmd="gpg1"
else
gpgcmd="gpg"
fi
do_generate()
{
key_size=4096
rsync_conf="$BASTION_ETC_DIR/osh-encrypt-rsync.conf.d/50-gpg-bastion-key.conf"
if [ -e "$rsync_conf" ]; then
echo "$rsync_conf already exists, aborting!" >&2
exit 1
fi
test -d "$BASTION_ETC_DIR/osh-encrypt-rsync.conf.d" || mkdir "$BASTION_ETC_DIR/osh-encrypt-rsync.conf.d"
sign_key_pass=$(perl -e '$p .= chr(int(rand(93))+33) for (1..16); $p =~ s{["\\]}{~}g; print "$p"')
printf "Key-Type: RSA\\nKey-Length: $key_size\\nSubkey-Type: RSA\\nSubkey-Length: $key_size\\nName-Real: %s\\nName-Comment: Bastion signing key\\nName-Email: %s\\nExpire-Date: 0\\nPassphrase: %s\\n%%echo Generating GPG key, it'll take some time.\\n%%commit\\n%%echo done\\n" "$(hostname)" "root@$(hostname)" "$sign_key_pass" | $gpgcmd --gen-key --batch
# get the id of the key we just generated
gpgid=$($gpgcmd --with-colons --list-keys "$(hostname) (Bastion signing key) <root@$(hostname)>" | awk -F: '/^pub:/ { print $5; exit; }')
if [ -z "$gpgid" ]; then
echo "Error while generating key, couldn't find the ID in gpg --list-keys :(" >&2
return 1
fi
cat > "$rsync_conf" <<EOF
# autogenerated with $0 at $(date)
{
"signing_key_passphrase": "$sign_key_pass",
"signing_key": "$gpgid"
}
EOF
chown "$UID0":"$GID0" "$rsync_conf"
chmod 600 "$rsync_conf"
echo
echo "Configuration file $rsync_conf updated:"
echo "8<---8<---8<---8<---8<---8<--"
cat "$rsync_conf"
echo "--->8--->8--->8--->8--->8--->8"
echo
echo Done.
}
do_import()
{
rsync_conf="$BASTION_ETC_DIR/osh-encrypt-rsync.conf.d/50-gpg-admins-key.conf"
if [ -e "$rsync_conf" ]; then
echo "$rsync_conf already exists, aborting!" >&2
exit 1
fi
test -d "$BASTION_ETC_DIR/osh-encrypt-rsync.conf.d" || mkdir "$BASTION_ETC_DIR/osh-encrypt-rsync.conf.d"
backup_conf="$BASTION_ETC_DIR/osh-backup-acl-keys.conf.d/50-gpg.conf"
if [ -e "$backup_conf" ]; then
echo "$backup_conf already exists, aborting!" >&2
exit 1
fi
test -d "$BASTION_ETC_DIR/osh-backup-acl-keys.conf.d" || mkdir "$BASTION_ETC_DIR/osh-backup-acl-keys.conf.d"
keys_before=$(mktemp)
# shellcheck disable=SC2064
trap "rm -f $keys_before" EXIT INT
$gpgcmd --with-colons --list-keys | grep ^pub: | awk -F: '{print $5}' > "$keys_before"
echo "Paste the admins public GPG key:"
$gpgcmd --import
newkey=''
for key in $($gpgcmd --with-colons --list-keys | grep ^pub: | awk -F: '{print $5}'); do
grep -qw "$key" "$keys_before" || newkey="$key"
done
if [ -z "$newkey" ]; then
echo "Couldn't find which key you imported, aborting" >&2
return 1
fi
echo "Found generated key with ID: $newkey"
fpr=$($gpgcmd --with-colons --fingerprint --list-keys "$newkey" | awk -F: '/^fpr:/ {print $10 ; exit}')
if [ -z "$fpr" ]; then
echo "Couldn't find the fingerprint of the generated key $newkey, aborting" >&2
return 1
fi
echo "Found generated key fingerprint: $fpr"
echo "Trusting this key..."
$gpgcmd --import-ownertrust <<< "$fpr:6:"
cat > "$rsync_conf" <<EOF
# autogenerated with $0 at $(date)
{
"recipients": [
[ "$newkey" ]
]
}
EOF
chown "$UID0":"$GID0" "$rsync_conf"
chmod 600 "$rsync_conf"
echo
echo "Configuration file $rsync_conf updated:"
echo "8<---8<---8<---8<---8<---8<--"
cat "$rsync_conf"
echo "--->8--->8--->8--->8--->8--->8"
cat > "$backup_conf" <<EOF
# autogenerated with $0 at $(date)
GPGKEYS='$newkey'
EOF
chown "$UID0":"$GID0" "$backup_conf"
chmod 600 "$backup_conf"
echo
echo "Configuration file $backup_conf updated:"
echo "8<---8<---8<---8<---8<---8<--"
cat "$backup_conf"
echo "--->8--->8--->8--->8--->8--->8"
echo
echo Done.
}
if [ "$1" = "--import" ]; then
do_import; exit $?
elif [ "$1" = "--generate" ]; then
do_generate; exit $?
fi
echo "Usage: $0 <--import|--generate>"
echo
echo "Use --generate to generate a new GPG keypair for bastion signing"
echo "Use --import to import the administrator GPG key you've generated on your desk (ttyrecs, keys and acls backups will be encrypted to it)"
exit 0

44
bin/admin/unlock-home.sh Executable file
View file

@ -0,0 +1,44 @@
#! /usr/bin/env bash
# vim: set filetype=sh ts=4 sw=4 sts=4 et:
CONFIGFILE=/etc/bastion/luks-config.sh
# shellcheck source=etc/bastion/luks-config.sh.dist
. "$CONFIGFILE"
do_mount()
{
mount "$MOUNTPOINT"; ret=$?
if [ $ret -eq 0 ] ; then
echo "Success!"
else
echo "Failure... is $MOUNTPOINT correctly specified in /etc/fstab?"
fi
exit $ret
}
if [ -z "$DEV_ENCRYPTED" ] || [ -z "$UNLOCKED_NAME" ] || [ -z "$MOUNTPOINT" ] || [ ! -d "$MOUNTPOINT" ] || [ ! -b "$DEV_ENCRYPTED" ] ; then
echo "Not configured or badly configured (check $CONFIGFILE), nothing to do."
exit 0
fi
if [ -e "$MOUNTPOINT/allowkeeper" ] ; then
echo "Already unlocked and mounted"
exit 0
fi
DEV_UNLOCKED="/dev/disk/by-id/dm-name-$UNLOCKED_NAME"
if [ -e "$DEV_UNLOCKED" ] ; then
echo "Already unlocked ($DEV_UNLOCKED), mounting..."
do_mount
fi
echo "Mouting $DEV_ENCRYPTED as $UNLOCKED_NAME"
cryptsetup luksOpen "$DEV_ENCRYPTED" "$UNLOCKED_NAME"
sleep 1
if [ -e "$DEV_UNLOCKED" ] ; then
echo "Mounting..."
do_mount
else
echo "Partition still encrypted, bad password?"
exit 1
fi

125
bin/cron/osh-backup-acl-keys.sh Executable file
View file

@ -0,0 +1,125 @@
#! /usr/bin/env bash
# vim: set filetype=sh ts=4 sw=4 sts=4 et:
set -e
umask 077
basedir=$(readlink -f "$(dirname "$0")"/../..)
# shellcheck source=lib/shell/functions.inc
. "$basedir"/lib/shell/functions.inc
if command -v gpg1 >/dev/null 2>&1; then
gpgcmd="gpg1"
else
gpgcmd="gpg"
fi
config_list=''
if [ -f "$BASTION_ETC_DIR/osh-backup-acl-keys.conf" ]; then
config_list="$BASTION_ETC_DIR/osh-backup-acl-keys.conf"
fi
if [ -d "$BASTION_ETC_DIR/osh-backup-acl-keys.conf.d" ]; then
config_list="$config_list $(find "$BASTION_ETC_DIR/osh-backup-acl-keys.conf.d" -mindepth 1 -maxdepth 1 -type f -name "*.conf" | sort)"
fi
if [ -z "$config_list" ]; then
_err "No configuration loaded, aborting"
exit 1
fi
# load the config files only if they're owned by root:root and mode is o-rwx
for file in $config_list; do
if [ "$(find "$file" -uid 0 -gid 0 ! -perm /o+rwx | wc -l)" = 1 ] ; then
# shellcheck source=etc/bastion/osh-backup-acl-keys.conf.dist
. "$file"
else
_err "Configuration file not secure ($file), aborting."
exit 1
fi
done
# shellcheck disable=SC2153
if [ -n "$LOGFILE" ] ; then
exec &>> >(tee -a "$LOGFILE")
fi
if [ -z "$DESTDIR" ] ; then
_err "$0: Missing DESTDIR in configuration, aborting."
exit 1
fi
if ! echo "$DAYSTOKEEP" | grep -Eq '^[0-9]+$' ; then
_err "$0: Invalid specified DAYSTOKEEP value ($DAYSTOKEEP), aborting."
exit 1
fi
_log "Starting backup..."
[ -d "$DESTDIR" ] || mkdir -p "$DESTDIR"
tarfile="$DESTDIR/backup-$(date +'%Y-%m-%d').tar.gz"
_log "Creating $tarfile..."
supp_entries=""
for entry in /root/.gnupg /var/otp
do
[ -e "$entry" ] && supp_entries="$supp_entries $entry"
done
# SC2086: we don't want to quote $supp_entries, we want it expanded
# shellcheck disable=SC2086
tar czf "$tarfile" -p --xattrs --acls --one-file-system --numeric-owner \
--exclude=".encrypt" \
--exclude="ttyrec" \
--exclude="*.sqlite" \
--exclude="*.log" \
--exclude="*.ttyrec" \
--exclude="*.gpg" \
--exclude="*.gz" \
--exclude="*.zst" \
/home/ /etc/passwd /etc/group /etc/shadow /etc/gshadow /etc/bastion /etc/ssh $supp_entries 2>/dev/null; ret=$?
if [ $ret -eq 0 ]; then
_log "File created"
else
_err "Error while creating file (sysret=$ret)"
fi
encryption_worked=0
if [ -n "$GPGKEYS" ] ; then
cmdline=""
for recipient in $GPGKEYS
do
cmdline="$cmdline -r $recipient"
done
# just in case, encrypt all .tar.gz files we find in $DESTDIR
while IFS= read -r -d '' file
do
_log "Encrypting $file..."
rm -f "$file.gpg" # if the gpg file already exists, remove it
# shellcheck disable=SC2086
if $gpgcmd --encrypt $cmdline "$file" ; then
encryption_worked=1
shred -u "$file" 2>/dev/null || rm -f "$file"
else
_err "Encryption failed"
fi
done < <(find "$DESTDIR/" -mindepth 1 -maxdepth 1 -type f -name 'backup-????-??-??.tar.gz' -print0)
else
_warn "$tarfile will not be encrypted! (no GPGKEYS specified)"
fi
# push to remote if needed
if [ -n "$PUSH_REMOTE" ] && [ "$encryption_worked" = 1 ] && [ -r "$tarfile.gpg" ] ; then
_log "Pushing backup file ($tarfile.gpg) remotely..."
# shellcheck disable=SC2086
scp $PUSH_OPTIONS "$tarfile.gpg" "$PUSH_REMOTE"; ret=$?
if [ $ret -eq 0 ]; then
_log "Push done"
else
_err "Push failed (sysret=$ret)"
fi
fi
# cleanup
_log "Cleaning up old backups..."
find "$DESTDIR/" -mindepth 1 -maxdepth 1 -type f -name 'backup-????-??-??.tar.gz' -mtime +"$DAYSTOKEEP" -delete
find "$DESTDIR/" -mindepth 1 -maxdepth 1 -type f -name 'backup-????-??-??.tar.gz.gpg' -mtime +"$DAYSTOKEEP" -delete
_log "Done"
exit 0

View file

@ -0,0 +1,36 @@
#! /usr/bin/env bash
# vim: set filetype=sh ts=4 sw=4 sts=4 et:
basedir=$(readlink -f "$(dirname "$0")"/../..)
# shellcheck source=lib/shell/functions.inc
. "$basedir"/lib/shell/functions.inc
LOG_FACILITY=local6
_log "Compressing old sqlite databases..."
while IFS= read -r -d '' sqlite
do
_log "Working on $sqlite..."
if ! gzip "$sqlite"; then
_log "Error while trying to compress $sqlite"
fi
done < <(find /home/ -mindepth 2 -maxdepth 2 -type f -name "*-log-??????.sqlite" -mtime +31 -print0)
# also compress homedir logs that haven't been touched since 30 days, every day
while IFS= read -r -d '' log
do
_log "Working on $log..."
command -v chattr >/dev/null && chattr -a "$log"
if ! gzip "$log"; then
_log "Error while trying to compress $log"
fi
done < <(find /home/ -mindepth 2 -maxdepth 2 -type f -name "*-log-??????.log" -mtime +31 -print0)
if command -v chattr >/dev/null; then
# then protect back all the logs
_log "Setting +a back on all the logs"
find /home/ -mindepth 2 -maxdepth 2 -type f -name "*-log-??????.log" -print0 | xargs -r0 chattr +a --
fi
_log "Done"

509
bin/cron/osh-encrypt-rsync.pl Executable file
View file

@ -0,0 +1,509 @@
#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use strict;
use warnings;
use GnuPG;
use File::Temp;
use File::Basename;
use File::Find;
use File::Path;
use Getopt::Long;
use Fcntl qw{ :flock };
use File::Basename;
use lib dirname(__FILE__) . '/../../lib/perl';
use OVH::Bastion;
use OVH::SimpleLog;
my %config;
my ($dryRun, $configTest, $forceRsync, $noDelete, $encryptOnly, $rsyncOnly, $verbose);
local $| = 1;
my $isoldversion = ($GnuPG::VERSION ge '0.18') ? 0 : 1;
sub test_config {
# normalize / define defaults / quick checks
$config{'trace'} = $config{'trace'} ? 1 : 0;
if (not exists $config{'recipients'}) {
_err "config error: recipients must be defined";
return 1;
}
if (ref $config{'recipients'} ne 'ARRAY') {
_err "config error: recipients must be an array of array of GPG key IDs! (layer 1)";
return 1;
}
if (my @intruders = grep { ref $config{'recipients'}[$_] ne 'ARRAY' } 0 .. $#{$config{'recipients'}}) {
local $" = ', ';
_err "config error: recipients must be an array of array of GPG key IDs! (layer 2, indexes @intruders)";
return 1;
}
if ($config{'encrypt_and_move_delay_days'} !~ /^\d+$/) {
_err "config error: encrypt_and_move_delay_days is not a positive integer!";
return 1;
}
if ($config{'rsync_delay_before_remove_days'} !~ /^\d+$/) {
_err "config error: rsync_delay_before_remove_days is not a positive integer!";
return 1;
}
# ok, check if my gpg conf is good
my $input = File::Temp->new(UNLINK => 1, TMPDIR => 1);
print {$input} time();
close($input);
_log "Testing signature with key $config{signing_key}... ";
eval {
my $gpgtest = GnuPG->new(trace => $config{'trace'});
my $outfile = File::Temp->new(UNLINK => 1, TMPDIR => 1);
# first, check we can sign
$gpgtest->sign(plaintext => $input . "", output => $outfile . "", "local-user" => $config{signing_key}, passphrase => $config{signing_key_passphrase});
if (not -s $outfile) {
die "Couldn't sign with the specified key $config{signing_key}, check your configuration";
}
};
if ($@) {
if ($@ =~ /BAD_PASSPHRASE/) {
_err "Bad passphrase for signing key $config{signing_key}";
return 1;
}
elsif ($@ =~ /expected NEED_PASSPHRASE/) {
_err "Signing key $config{signing_key} was not found";
return 1;
}
_err "When testing signing key: $@";
return 1;
}
my %recipients_uniq;
foreach my $recipient_list (@{$config{'recipients'}}) {
foreach my $recipient (@$recipient_list) {
$recipients_uniq{$recipient}++;
}
}
eval {
foreach my $recipient (keys %recipients_uniq) {
_log "Testing encryption for recipient $recipient... ";
my $gpgtest = GnuPG->new(trace => $config{'trace'});
# then, check we can encrypt to each of the recipients
my $outfile = File::Temp->new(UNLINK => 1, TMPDIR => 1);
my $recipientparam = $isoldversion ? $recipient : [$recipient, $recipient];
$gpgtest->encrypt(plaintext => $input . "", output => $outfile . "", recipient => $recipientparam);
if (not -s $outfile) {
die "Couldn't encrypt for the specified recipient <$recipient>, check your configuration";
}
}
};
if ($@) {
_err "When testing recipient key: $@";
return 1;
}
if ($isoldversion and keys %recipients_uniq > 1) {
_err "You have an old version of the GnuPG module that doesn't support multiple recipients, sorry.";
return 1;
}
_log "Testing encryption for all recipients + signature... ";
eval {
my $gpgtest = GnuPG->new(trace => $config{'trace'});
# then, encrypt to all the recipients, sign, and check the signature
my $outfile = File::Temp->new(UNLINK => 1, TMPDIR => 1);
my $recipientparam = $isoldversion ? (keys %recipients_uniq)[0] : [keys %recipients_uniq];
$gpgtest->encrypt(
plaintext => $input . "",
output => $outfile . "",
recipient => $recipientparam,
sign => 1,
"local-user" => $config{signing_key},
passphrase => $config{signing_key_passphrase}
);
if (not -s $outfile) {
die "Couldn't encrypt and sign, check your configuration";
}
};
if ($@) {
_err "When testing encrypt+sign: $@";
return 1;
}
_log "Config test passed";
return 0;
}
sub encrypt_multi {
my %params = @_;
my $source_file = $params{'source_file'};
my $destination_directory = $params{'destination_directory'};
my $remove_source_on_success = $params{'remove_source_on_success'} || 0;
my $outfile = $source_file;
$outfile =~ s!^/home/!$destination_directory/!;
my $outdir = File::Basename::dirname($outfile);
if (!-e $outdir) {
_log "Creating $outdir";
$dryRun or File::Path::mkpath(File::Basename::dirname($outfile), 0, oct(700));
}
my $layers = scalar(@{$config{'recipients'}});
_log "Encrypting $source_file to $outfile" . ".gpg" x $layers;
my $layer = 0;
my $current_source_file = $source_file;
my $current_destination_file = $outfile . '.gpg';
my $success = 1;
foreach my $recipients_array (@{$config{'recipients'}}) {
$layer++;
_log " ... encrypting $current_source_file to $current_destination_file" if $verbose;
my $error = encrypt_once(
source_file => $current_source_file,
destination_file => $current_destination_file,
recipients => ($isoldversion ? $recipients_array->[0] : $recipients_array)
);
if ($layer > 1 and $layer <= $layers) {
# transient file
_log " ... deleting transient file $current_source_file" if $verbose;
$dryRun or unlink $current_source_file;
}
if ($error) {
$success = 0;
last;
}
$current_source_file = $current_destination_file;
$current_destination_file .= '.gpg';
}
if ($success and $remove_source_on_success) {
_log " ... removing source file $source_file" if $verbose;
$dryRun or unlink $source_file;
}
return !$success;
}
sub encrypt_once {
my %params = @_;
my $source_file = $params{'source_file'};
my $destination_file = $params{'destination_file'};
my $recipients = $params{'recipients'};
if (not -f $source_file and not $dryRun) {
_err "encrypt_once: source file $source_file is not a file!";
return 1;
}
# don't care ... overwrite
# TODO check if GnuPG overwrites silently or dies
#if (-f $destination_file)
#{
# _err "encrypt_once: destination file $destination_file already exists!";
# return 1;
#}
my $GPG = GnuPG->new(trace => $config{'trace'});
eval {
$dryRun
or $GPG->encrypt(
plaintext => $source_file,
output => $destination_file,
recipient => $recipients,
sign => 1,
"local-user" => $config{signing_key},
passphrase => $config{signing_key_passphrase}
);
};
if ($@) {
_err "encrypt_once: when working on $source_file => $destination_file, got error $@";
return 1;
}
return 0; # no error
}
my $openedFiles = undef;
sub potentially_work_on_this_file {
# file must be a ttyrec file or an osh_http_proxy_ttyrec-ish file
my $filetype;
$filetype = 'ttyrec' if m{^/home/[^/]+/ttyrec/[^/]+/[A-Za-z0-9._-]+(\.ttyrec(\.zst)?)?$};
$filetype = 'proxylog' if m{^/home/[^/]+/ttyrec/[^/]+/\d+-\d+-\d+\.txt$};
$filetype or return;
# must exist and be a file
-f or return;
my $file = $_;
# first, check if we populated $openedFiles as a hashref
if (ref $openedFiles ne 'HASH') {
$openedFiles = {};
if (open(my $fh_lsof, '-|', "lsof -a -n -c ttyrec -- /home/")) {
while (<$fh_lsof>) {
chomp;
m{\s(/home/[^/]+/ttyrec/\S+)$} and $openedFiles->{$1} = 1;
}
close($fh_lsof);
_log "Found " . (scalar keys %$openedFiles) . " opened ttyrec files we won't touch";
}
else {
_warn "Error trying to get the list of opened ttyrec files, we might rotate opened files!";
}
}
# still open ? don't touch
if (exists $openedFiles->{$file}) {
_log "File $file is still opened by ttyrec, skipping";
return;
}
# and must be older than encrypt_and_move_delay_days days
my $mtime = (stat($file))[9];
if ($mtime > time() - 86400 * $config{'encrypt_and_move_delay_days'}) {
_log "File $file is too recent, skipping" if $verbose;
return;
}
# for proxylog, never touch a file that's < 86400 sec old (because we might still write to it)
if ($filetype eq 'proxylog' and $mtime > time() - 86400) {
_log "File $file is too recent (proxylog), skipping" if $verbose;
return;
}
my $error = encrypt_multi(source_file => $file, destination_directory => $config{'encrypt_and_move_to_directory'}, remove_source_on_success => not $noDelete);
if ($error) {
_err "Got an error for $file, skipping!";
}
return;
}
sub directory_filter { ## no critic (RequireArgUnpacking)
# /home ? check the subdirs
if ($File::Find::dir eq '/home') {
my @out = ();
foreach (@_) {
if (-d "/home/$_/ttyrec") {
#_log("DBG: filtering /home, $_ is OK");
push @out, $_ if -d "/home/$_/ttyrec";
}
else {
; #_log("DBG: filtering /home, $_ is COMPLETELY OUT");
}
}
return @out;
}
if ($File::Find::dir =~ m{^/home/[^/]+($|/ttyrec)}) {
#_log("DBG: yep ok $File::Find::dir");
return @_;
}
#_log("DBG: quickill $File::Find::dir");
return ();
}
sub main {
_log "Starting...";
if (
not GetOptions(
"dry-run" => \$dryRun,
"config-test" => \$configTest,
"no-delete" => \$noDelete,
"encrypt-only" => \$encryptOnly,
"rsync-only" => \$rsyncOnly,
"force-rsync" => \$forceRsync,
"verbose" => \$verbose,
)
)
{
_err "Error while parsing command-line options";
return 1;
}
# we can have CONFIGDIR/osh-encrypt-rsync.conf
# but also CONFIGDIR/osh-encrypt-rsync.conf.d/*
# later files override the previous ones, item by item
my $fnret;
my $lockfile;
my @configfilelist;
if (-f -r OVH::Bastion::main_configuration_directory() . "/osh-encrypt-rsync.conf") {
push @configfilelist, OVH::Bastion::main_configuration_directory() . "/osh-encrypt-rsync.conf";
}
if (-d -x OVH::Bastion::main_configuration_directory() . "/osh-encrypt-rsync.conf.d") {
if (opendir(my $dh, OVH::Bastion::main_configuration_directory() . "/osh-encrypt-rsync.conf.d")) {
my @subfiles = map { OVH::Bastion::main_configuration_directory() . "/osh-encrypt-rsync.conf.d/" . $_ } grep { /\.conf$/ } readdir($dh);
closedir($dh);
push @configfilelist, sort @subfiles;
}
}
if (not @configfilelist) {
_err "Error, no config file found!";
return 1;
}
foreach my $configfile (@configfilelist) {
_log "Configuration: loading configfile $configfile...";
$fnret = OVH::Bastion::load_configuration_file(
file => $configfile,
secure => 1,
);
if (not $fnret) {
_err "Error while loading configuration from $configfile, aborting (" . $fnret->msg . ")";
return 1;
}
foreach my $key (keys %{$fnret->value}) {
$config{$key} = $fnret->value->{$key};
}
# we'll be using our own config file as a handy flock() backend
$lockfile = $configfile if not defined $lockfile;
}
$verbose ||= $config{'verbose'};
# ensure no other copy of myself is already running
# except if we are in rsync-only mode (concurrency is then not a problem)
my $lockfh;
if (not $rsyncOnly) {
if (!open($lockfh, '<', $lockfile)) {
# flock() needs a file handler
_log "Couldn't open config file, aborting";
return 1;
}
if (!flock($lockfh, LOCK_EX | LOCK_NB)) {
_log "Another instance is running, aborting this one!";
return 1;
}
}
# ensure the various config files defined all the keywords we need
foreach my $keyword (
qw{ logfile signing_key signing_key_passphrase recipients encrypt_and_move_to_directory encrypt_and_move_delay_days rsync_destination rsync_delay_before_remove_days })
{
next if defined $config{$keyword};
_err "Missing mandatory configuration item '$keyword', aborting";
return 1;
}
OVH::SimpleLog::setLogFile($config{'logfile'}) if $config{'logfile'};
OVH::SimpleLog::setSyslog($config{'syslog_facility'}) if $config{'syslog_facility'};
if ($forceRsync) {
config { 'rsync_delay_days' } = 0;
}
if (test_config() != 0) {
_err "Config test failed, aborting";
return 1;
}
if ($configTest) {
return 0;
}
if ($dryRun) {
_log "Dry-run mode enabled, won't actually encrypt, move or delete files!";
}
if (not $rsyncOnly) {
_log "Looking for files in /home/ ...";
File::Find::find(
{
no_chdir => 1,
preprocess => \&directory_filter,
wanted => \&potentially_work_on_this_file
},
"/home/",
);
}
if (not($encryptOnly || $config{'encrypt_only'}) and $config{'rsync_destination'}) {
my @command;
my $sysret;
if (!-d $config{'encrypt_and_move_to_directory'} && $dryRun) {
_log "DRYRUN: source directory doesn't exist, substituting with another one (namely the config directory which we know exists), just to try the rsync in dry-run mode";
$config{'encrypt_and_move_to_directory'} = '/etc/cron.d/';
}
if (!-d $config{'encrypt_and_move_to_directory'}) {
_log "Nothing to rsync as the rsync source dir doesn't exist";
}
else {
_log "Now rsyncing files to remote host ...";
@command = qw{ rsync --prune-empty-dirs --one-file-system -a };
push @command, '-v' if $verbose;
if ($config{'rsync_rsh'}) {
push @command, '--rsh', $config{'rsync_rsh'};
}
if ($dryRun) {
push @command, '--dry-run';
}
push @command, $config{'encrypt_and_move_to_directory'} . '/';
push @command, $config{'rsync_destination'} . '/';
_log "Launching the following command: @command";
$sysret = system(@command);
if ($sysret != 0) {
_err "Error while rsyncing, stopping here";
return 1;
}
# now run rsync again BUT only with files having mtime +rsync_delay_before_remove_days AND specifying --remove-source-files
# this way only files old enough AND successfully transferred to the other side will be removed
if (!$dryRun) {
my $prevdir = $ENV{'PWD'};
if (not chdir $config{'encrypt_and_move_to_directory'}) {
_err "Error while trying to chdir to " . $config{'encrypt_and_move_to_directory'} . ", aborting";
return 1;
}
_log "Building a list of rsynced files to potentially delete (older than " . $config{'rsync_delay_before_remove_days'} . " days)";
my $cmdstr = "find . -xdev -type f -name '*.gpg' -mtime +" . ($config{'rsync_delay_before_remove_days'} - 1) . " -print0 | rsync -" . ($verbose ? 'v' : '') . "a ";
if ($config{'rsync_rsh'}) {
$cmdstr .= "--rsh '" . $config{'rsync_rsh'} . "' ";
}
if ($dryRun) {
$cmdstr .= "--dry-run ";
}
$cmdstr .= "--remove-source-files --files-from=- --from0 " . $config{'encrypt_and_move_to_directory'} . '/' . " " . $config{'rsync_destination'} . '/';
_log "Launching the following command: $cmdstr";
$sysret = system($cmdstr);
if ($sysret != 0) {
_err "Error while rsyncing for deletion, stopping here";
return 1;
}
# remove empty directories
_log "Removing now empty directories...";
system("find " . $config{'encrypt_and_move_to_directory'} . " -type d ! -wholename " . $config{'encrypt_and_move_to_directory'} . " -delete 2>/dev/null")
; # errors would be printed for non empty dirs, we don't care
chdir $prevdir;
}
}
}
_log "Done";
return 0;
}
exit main();

View file

@ -0,0 +1,47 @@
#! /usr/bin/env bash
# vim: set filetype=sh ts=4 sw=4 sts=4 et:
set -e
basedir=$(readlink -f "$(dirname "$0")"/../..)
# shellcheck source=lib/shell/functions.inc
. "$basedir"/lib/shell/functions.inc
LOG_FACILITY=local6
_log "Terminating lingering sessions..."
tokill=''
nb=0
# shellcheck disable=SC2162
while read etimes pid tty
do
if [ "$tty" = "?" ] && [ "$etimes" -gt 86400 ]; then
tokill="$tokill $pid"
(( nb++ ))
fi
done < <(ps -C ttyrec -o etimes,pid,tty --no-header)
if [ -n "$tokill" ]; then
# shellcheck disable=SC2086
kill $tokill
_log "Terminated $nb orphan ttyrec sessions (pids$tokill)"
fi
tokill=''
nb=0
# shellcheck disable=SC2162
while read etimes pid tty user
do
if [ "$tty" = "?" ] && [ "$user" != "root" ] && [ "$etimes" -gt 86400 ]; then
if [ "$(ps --no-header --ppid "$pid" | wc -l)" = 0 ]; then
tokill="$tokill $pid"
(( nb++ ))
fi
fi
done < <(ps -C sshd --no-header -o etimes,pid,tty,user)
if [ -n "$tokill" ]; then
# shellcheck disable=SC2086
kill $tokill
_log "Terminated $nb orphan sshd sessions (pids$tokill)"
fi
_log "Done"

View file

@ -0,0 +1,52 @@
#! /usr/bin/env bash
# vim: set filetype=sh ts=4 sw=4 sts=4 et:
set -e
umask 077
basedir=$(readlink -f "$(dirname "$0")"/../..)
# shellcheck source=lib/shell/functions.inc
. "$basedir"/lib/shell/functions.inc
LOG_FACILITY=local6
_log "Checking orphaned home directories..."
while IFS= read -r -d '' dir
do
test -d "/home/oldkeeper/orphaned" || mkdir -p "/home/oldkeeper/orphaned"
archive="/home/oldkeeper/orphaned/$(basename "$dir").at-$(date +%s).by-orphaned-homedir-script.tar.gz"
_log "Found orphaned $dir [$(ls -ld "$dir")], archiving..."
chmod 0700 /home/oldkeeper/orphaned
if [ "$OS_FAMILY" = "Linux" ]; then
find "$dir" -mindepth 1 -maxdepth 1 -type f -name "*.log" -print0 | xargs -r0 chattr -a
fi
# remove empty directories if we have some
find "$dir" -type d -delete 2>/dev/null || true
acls_param=''
[ "$OS_FAMILY" = "Linux" ] && acls_param='--acls'
[ "$OS_FAMILY" = "FreeBSD" ] && acls_param='--acls'
set +e
tar czf "$archive" $acls_param --one-file-system -p --remove-files --exclude=ttyrec "$dir" 2>/dev/null; ret=$?
set -e
if [ $ret -ne 0 ]; then
# $? can be 2 if we can't delete because ttyrec dir remains so it might not be a problem
if [ $ret -eq 2 ] && [ -s "$archive" ] && [ -d "$dir" ] && [ "$(find "$dir" -name ttyrec -prune -o -print | wc -l)" = 1 ]; then
# it's ok. we chown all to root to avoid orphan UID/GID and we let the backup script take care of those
# if we still have files under $dir/ttyrec, chown all them to root:root to avoid orphan UID/GID,
# and just wait for them to be encrypted/rsynced out of the bastion by the usual ttyrec archiving script
_log "Archived $dir to $archive"
chmod 0 "$archive"
chown -R root:root "$dir"
_warn "Some files remain in $dir, we chowned everything to root"
else
_err "Couldn't archive $dir to $archive"
fi
else
_log "Archived $dir to $archive"
chmod 0 "$archive"
fi
done < <(find /home/ -mindepth 1 -maxdepth 1 -type d -nouser -nogroup -mmin +3 -print0)
_log "Done"
exit 0

105
bin/cron/osh-piv-grace-reaper.pl Executable file
View file

@ -0,0 +1,105 @@
#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;
use File::Basename;
use lib dirname(__FILE__) . '/../../lib/perl';
use OVH::Bastion;
use OVH::Result;
use OVH::SimpleLog;
my $fnret;
$fnret = OVH::Bastion::load_configuration_file(
file => OVH::Bastion::main_configuration_directory() . "/osh-piv-grace-reaper.conf",
secure => 1,
keywords => [qw{ SyslogFacility }],
);
my $config;
if (not $fnret) {
_err "Error while loading configuration, continuing anyway with default values...";
}
else {
$config = $fnret->value;
if (ref $config ne 'HASH') {
_err "Invalid data returned while loading configuration, continuing anyway with default values...";
}
}
# logging
if ($config && $config->{'SyslogFacility'}) {
OVH::SimpleLog::setSyslog($config->{'SyslogFacility'});
}
_log "Looking for accounts with a PIV grace...";
# loop through all the accounts, and only work on those that have a grace period set
$fnret = OVH::Bastion::get_account_list();
if (!$fnret) {
_err "Couldn't get account list: " . $fnret->msg;
exit 1;
}
# this'll be used in syslog
$ENV{'UNIQID'} = OVH::Bastion::generate_uniq_id()->value;
foreach my $account (%{$fnret->value}) {
# if account doesn't have PIV grace, don't bother
$fnret = OVH::Bastion::account_config(account => $account, public => 1, key => OVH::Bastion::OPT_ACCOUNT_INGRESS_PIV_GRACE);
next if !$fnret;
# we have PIV grace set for this account
my $expiry = $fnret->value;
my $human = OVH::Bastion::duration2human(seconds => ($expiry - time()))->value;
_log "Account $account has PIV grace expiry set to $expiry (" . $human->{'human'} . ")";
# is PIV grace TTL expired?
if (time() > $expiry) {
# it is, but if current policy is not set to enforce, it's useless
_log "... grace for $account is expired, is current policy set to enforced?";
$fnret = OVH::Bastion::account_config(account => $account, public => 1, key => OVH::Bastion::OPT_ACCOUNT_INGRESS_PIV_POLICY);
if (!$fnret || $fnret->value ne 'yes') {
# PIV grace expired but current policy is already relaxed, so just remove the grace flag
_log "... grace for $account is expired, but current policy is not set to enforced, removing grace...";
$fnret = OVH::Bastion::account_config(account => $account, public => 1, key => OVH::Bastion::OPT_ACCOUNT_INGRESS_PIV_GRACE, delete => 1);
if (!$fnret) {
_err "... couldn't remove grace flag for $account";
next;
}
# grace removed for this account, no change needed on keys because it wasn't set to enforced
next;
}
# PIV grace expired, we need to remove the non-PIV keys from the account's authorized_keys2 file
_log "... grace for $account is expired, enforcing PIV-keys only...";
OVH::SimpleLog::closeSyslog();
$fnret = OVH::Bastion::ssh_ingress_keys_piv_apply(action => "enable", account => $account);
if (!$fnret) {
_err "... failed to re-enforce PIV policy for $account ($fnret->msg)";
next;
}
if ($config && $config->{'SyslogFacility'}) {
OVH::SimpleLog::setSyslog($config->{'SyslogFacility'});
}
_log "... re-enforced PIV policy for $account";
# ok, now remove grace flag
$fnret = OVH::Bastion::account_config(account => $account, public => 1, key => OVH::Bastion::OPT_ACCOUNT_INGRESS_PIV_GRACE, delete => 1);
if (!$fnret) {
_err "... couldn't remove grace flag for $account";
}
else {
_log "... grace flag removed for $account";
}
}
else {
_log "... grace for $account is not expired yet, skipping...";
}
}
_log "Done";

36
bin/cron/osh-rotate-ttyrec.sh Executable file
View file

@ -0,0 +1,36 @@
#! /usr/bin/env bash
# vim: set filetype=sh ts=4 sw=4 sts=4 et:
set -e
basedir=$(readlink -f "$(dirname "$0")"/../..)
# shellcheck source=lib/shell/functions.inc
. "$basedir"/lib/shell/functions.inc
LOG_FACILITY=local6
if [ "$1" = "--big-only" ]; then
_log "Rotating big ttyrec files..."
tokill=''
nb=0
# shellcheck disable=SC2034
while read -r command pid user fd type device size node name
do
if echo "$size" | grep -qE '^[0-9]+$' && [ "$size" -gt 100000000 ]; then
tokill="$tokill $pid"
(( nb++ ))
fi
done < <(lsof -a -n -c ttyrec 2>/dev/null -- /home/ 2>/dev/null)
if [ -n "$tokill" ]; then
_log "Rotating $nb big ttyrec files..."
# shellcheck disable=SC2086
kill -USR1 $tokill
fi
else
_log "Rotating all ttyrec files..."
if pkill --signal USR1 ttyrec; then
_log "Rotation done"
else
_log "No ttyrec files to rotate"
fi
fi
_log "Done"

31
bin/dev/debug_toggle.sh Executable file
View file

@ -0,0 +1,31 @@
#! /usr/bin/env bash
# vim: set filetype=sh ts=4 sw=4 sts=4 et:
account=$1
toggle=$2
_help()
{
echo "$0 <account> <on|off>"
exit 1
}
[ -z "$toggle" ] && _help
if [ ! -d "/home/$account" ] ; then
echo "/home/$account not found"
exit 1
fi
if [ "$toggle" = on ] ; then
echo yes > "/home/$account/config.debug"
chown "$account":"$account" "/home/$account/config.debug"
echo "debug enabled for $account"
elif [ "$toggle" = off ] ; then
rm -f "/home/$account/config.debug"
echo "debug disabled for $account"
else
echo "Unknown toggle ($toggle)"
_help
fi
exit 0

48
bin/dev/perl-check.sh Executable file
View file

@ -0,0 +1,48 @@
#! /usr/bin/env bash
# vim: set filetype=sh ts=4 sw=4 sts=4 et:
basedir=$(readlink -f "$(dirname "$0")"/../..)
# shellcheck source=lib/shell/functions.inc
. "$basedir"/lib/shell/functions.inc
# skip this script entirely if asked
[ "$1" = "--test-quick" ] && [ "$2" = 1 ] && exit 0
cmdline='-Mstrict -Mwarnings'
(( fails=0 ))
action_doing "Checking perl files syntax"
for i in $(find "$basedir"/bin -type f ! -name "*.orig") $(find "$basedir"/lib/perl -type f -name "*.pm") $(find "$basedir"/lib/perl -type f -name "*.inc")
do
i=$(readlink -f "$i")
if head -n1 "$i" | grep -Eq '/perl|/env perl' || head -n2 "$i" | grep -Eq '^package ' ; then
# FIXME remove below block when we get rid of GnuPG perl module in below script
if [ "$i" = "$basedir/bin/cron/osh-encrypt-rsync.pl" ] && echo "$DISTRO_LIKE" | grep -qw -e rhel -e suse; then
action_detail "${BLUE}$i${NOC}: skipping"
continue
fi
action_detail "${BLUE}$i${NOC}"
if grep -q -- 'perl -T' "$i"; then
# shellcheck disable=SC2086
perl $cmdline -Tc "$i" 2>&1 | grep -v OK$
else
# shellcheck disable=SC2086
perl $cmdline -c "$i" 2>&1 | grep -v OK$
fi
[ "${PIPESTATUS[0]}" -ne 0 ] && (( fails++ ))
[ -n "$DEBUG" ] || continue
grep -q '^use warnings' "$i" && echo "(spurious use warnings in $i)"
grep -q '^use strict' "$i" && echo "(spurious use strict in $i)"
grep -q '^use common::sense;' "$i" || echo "(missing common::sense in $i)"
fi
done
if [ -x "$basedir/bin/dev/perl-use-all.pl" ] ; then
action_detail "Trying to \`use' all required perl modules"
"$basedir/bin/dev/perl-use-all.pl" || (( fails++ ))
fi
if [ "$fails" -ne 0 ] ; then
action_error "Got $fails errors"
else
action_done "success"
fi
exit "$fails"

19
bin/dev/perl-critic.sh Executable file
View file

@ -0,0 +1,19 @@
#! /usr/bin/env bash
# vim: set filetype=sh ts=4 sw=4 sts=4 et:
basedir=$(readlink -f "$(dirname "$0")"/../..)
# shellcheck source=lib/shell/functions.inc
. "$basedir"/lib/shell/functions.inc
cd "$basedir" || exit 1
action_doing "Checking perlcritic"
# shellcheck disable=SC2086
perlcritic --color -q -p "$(dirname "$0")"/perlcriticrc .; ret=$?
if [ "$ret" = 0 ]; then
# shellcheck disable=SC2119
action_done
else
action_error "perlcritic found errors"
exit 1
fi

48
bin/dev/perl-tidy.sh Executable file
View file

@ -0,0 +1,48 @@
#! /usr/bin/env bash
# vim: set filetype=sh ts=4 sw=4 sts=4 et:
basedir=$(readlink -f "$(dirname "$0")"/../..)
# shellcheck source=lib/shell/functions.inc
. "$basedir"/lib/shell/functions.inc
cd "$basedir" || exit 1
if [ "$1" = "test" ]; then
params=""
action_doing "Checking perl tidiness"
else
params="--backup-and-modify-in-place --backup-file-extension=/tidybak"
action_doing "Tidying perl files"
fi
# shellcheck disable=SC2086
find . -type f ! -name "*.tdy" ! -name "*.ERR" ! -name "$(basename $0)" -print0 | \
xargs -r0 grep -l 'set filetype=perl' -- | \
xargs -r perltidy --paren-tightness=2 --square-bracket-tightness=2 --brace-tightness=2 --maximum-line-length=180 $params
bad=""
nbbad=0
if [ "$1" = "test" ]; then
while IFS= read -r -d '' tdy
do
file=${tdy/.tdy/}
if ! cmp "$file" "$tdy"; then
diff -u "$file" "$tdy"
bad="$bad $file"
nbbad=$(( nbbad + 1 ))
action_error "... $file is not tidy!"
fi
rm -f "$tdy"
done < <(find . -name "*.tdy" -type f -print0)
if [ "$nbbad" = 0 ]; then
action_done ""
else
action_error "Found $nbbad untidy files"
fi
else
action_done ""
fi
exit $nbbad

56
bin/dev/perl-use-all.pl Executable file
View file

@ -0,0 +1,56 @@
#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use strict;
use warnings;
use Carp;
use CGI;
use common::sense;
use Config;
use Cwd;
use Data::Dumper;
use DBD::SQLite;
use Digest::MD5;
use Digest::SHA;
use Exporter;
use Fcntl;
use File::Basename;
use File::Copy;
use File::Find;
use File::Path;
use File::Temp;
use Getopt::Long;
use HTTP::Headers;
use HTTP::Message;
use HTTP::Request;
use IO::Compress::Gzip;
use IO::Handle;
use IO::Pipe;
use IO::Select;
use IO::Socket::SSL;
use IPC::Open2;
use IPC::Open3;
use JSON;
use List::Util;
use LWP::UserAgent;
use MIME::Base64;
use Net::IP;
use Net::Netmask;
use Net::Server::PreFork;
use Net::Server::PreForkSimple;
use POSIX;
use Scalar::Util;
use Socket;
use Storable;
use Symbol;
use Sys::Hostname;
use Sys::Syslog;
use Term::ANSIColor;
use Term::ReadKey;
use Term::ReadLine;
use Time::HiRes;
use Time::Piece;
use URI;
print "OK: all required Perl modules are present\n";

44
bin/dev/perlcriticrc Normal file
View file

@ -0,0 +1,44 @@
verbose = %f: [%p] %m at line %l, column %c.\n
severity = 2
[TestingAndDebugging::RequireUseStrict]
equivalent_modules = common::sense
[TestingAndDebugging::RequireUseWarnings]
equivalent_modules = common::sense
[Variables::RequireLocalizedPunctuationVars]
allow = %ENV %SIG $|
[ValuesAndExpressions::RequireNumberSeparators]
min_value = 100000
[Subroutines::RequireFinalReturn]
terminal_funcs = HEXIT osh_exit osh_ok
[ControlStructures::ProhibitDeepNests]
max_nests = 6
[-BuiltinFunctions::ProhibitBooleanGrep]
[-ControlStructures::ProhibitCascadingIfElse]
[-ControlStructures::ProhibitPostfixControls]
[-Documentation::RequirePodSections]
[-ErrorHandling::RequireCarping]
[-ErrorHandling::RequireCheckingReturnValueOfEval]
[-InputOutput::ProhibitExplicitStdin]
[-InputOutput::RequireBriefOpen]
[-InputOutput::RequireCheckedClose]
[-Modules::ProhibitExcessMainComplexity]
[-Modules::RequireFilenameMatchesPackage]
[-Modules::RequireVersionVar]
[-References::ProhibitDoubleSigils]
[-RegularExpressions::ProhibitComplexRegexes]
[-RegularExpressions::RequireDotMatchAnything]
[-RegularExpressions::RequireExtendedFormatting]
[-RegularExpressions::RequireLineBoundaryMatching]
[-Subroutines::ProhibitExcessComplexity]
[-ValuesAndExpressions::ProhibitConstantPragma]
[-ValuesAndExpressions::ProhibitEmptyQuotes]
[-ValuesAndExpressions::ProhibitMagicNumbers]
[-ValuesAndExpressions::ProhibitNoisyQuotes]
[-Variables::ProhibitPunctuationVars]

42
bin/dev/shell-check.sh Executable file
View file

@ -0,0 +1,42 @@
#! /usr/bin/env bash
# vim: set filetype=sh ts=4 sw=4 sts=4 et:
basedir=$(readlink -f "$(dirname "$0")"/../..)
# shellcheck source=lib/shell/functions.inc
. "$basedir"/lib/shell/functions.inc
unset dockertag
if [ "$1" = "docker" ]; then
dockertag=v0.7.1
fi
if [ -n "$2" ]; then
dockertag="$2"
fi
(( fails=0 ))
if [ -n "$dockertag" ]; then
action_doing "Checking shell files syntax using shellcheck:$dockertag docker"
else
action_doing "Checking shell files syntax"
fi
cd "$basedir" || exit 254
for i in $(find . -type f ! -name "*.swp" -print0 | xargs -r0 grep -l 'set filetype=sh')
do
action_detail "${BLUE}$i${NOC}"
if [ -n "$dockertag" ]; then
docker run --rm -v "$PWD:/mnt" "koalaman/shellcheck:$dockertag" -Calways -W 0 -x -o deprecate-which,avoid-nullary-conditions,add-default-case "$i"; ret=$?
else
shellcheck -x "$i"; ret=$?
fi
if [ "$ret" != 0 ]; then
(( fails++ ))
fi
done
if [ "$fails" -ne 0 ] ; then
action_error "Got $fails errors"
else
action_done "success"
fi
exit "$fails"

View file

@ -0,0 +1,83 @@
#! /usr/bin/perl -T
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
# KEYSUDOERS # as a gatekeeper, to be able to add the servers to /home/allowkeeper/ACCOUNT/allowed.partial.%GROUP% file
# KEYSUDOERS SUPEROWNERS, %%GROUP%-gatekeeper ALL=(allowkeeper) NOPASSWD: /usr/bin/env perl -T %BASEPATH%/bin/helper/osh-accountAddGroupServer --group %GROUP% *
# FILEMODE 0750
# FILEOWN root allowkeeper
#>HEADER
use common::sense;
use Getopt::Long;
use File::Basename;
use lib dirname(__FILE__) . '/../../lib/perl';
use OVH::Bastion;
local $| = 1;
#
# Globals
#
$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/pkg/bin';
my ($self) = $ENV{'SUDO_USER'} =~ m{^([a-zA-Z0-9._-]+)$};
if (not defined $self) {
if ($< == 0) {
$self = 'root';
}
else {
HEXIT('ERR_SUDO_NEEDED', msg => 'This command must be run under sudo');
}
}
# Fetch command options
my $fnret;
my ($result, @optwarns);
my ($account, $group, $ip, $user, $port, $action, $ttl, $forceKey);
eval {
local $SIG{__WARN__} = sub { push @optwarns, shift };
$result = GetOptions(
"account=s" => sub { $account //= $_[1] },
"group=s" => sub { $group //= $_[1] },
"ip=s" => sub { $ip //= $_[1] },
"user=s" => sub { $user //= $_[1] },
"port=i" => sub { $port //= $_[1] },
"action=s" => sub { $action //= $_[1] },
"ttl=i" => sub { $ttl //= $_[1] },
"force-key=s" => sub { $forceKey //= $_[1] },
);
};
if ($@) { die $@ }
if (!$result) {
local $" = ", ";
HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns");
}
if (not $action or not $ip or not $account or not $group) {
HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'action' or 'ip' or 'account' or 'group'");
}
#<HEADER
not defined $account and $account = $self;
#>PARAMS:ACTION
if (not grep { $action eq $_ } qw{ add del }) {
return R('ERR_INVALID_PARAMETER', msg => "expected 'add' or 'del' as an action");
}
#<PARAMS:ACTION
#>CODE
# access_modify validates all its parameters, don't do it ourselves here for clarity
$fnret = OVH::Bastion::access_modify(
way => 'groupguest',
account => $account,
group => $group,
action => $action,
user => $user,
ip => $ip,
port => $port,
ttl => $ttl,
forceKey => $forceKey
);
HEXIT($fnret);

436
bin/helper/osh-accountCreate Executable file
View file

@ -0,0 +1,436 @@
#! /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 root root
#>HEADER
use common::sense;
use Getopt::Long;
use Sys::Hostname ();
use File::Basename;
use lib dirname(__FILE__) . '/../../lib/perl';
use OVH::Bastion;
local $| = 1;
#
# Globals
#
$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/pkg/bin';
my ($self) = $ENV{'SUDO_USER'} =~ m{^([a-zA-Z0-9._-]+)$};
if (not defined $self) {
if ($< == 0) {
$self = 'root';
}
else {
HEXIT('ERR_SUDO_NEEDED', msg => 'This command must be run under sudo');
}
}
# Fetch command options
my $fnret;
my ($result, @optwarns);
my ($type, $account, $realmFrom, $uid, @pubKeys, $comment, $alwaysActive, $uidAuto, $oshOnly, $immutableKey, $ttl);
eval {
local $SIG{__WARN__} = sub { push @optwarns, shift };
$result = GetOptions(
"type=s" => sub { $type //= $_[1] },
"from=s" => sub { $realmFrom //= $_[1] },
"uid=s" => sub { $uid //= $_[1] },
"account=s" => sub { $account //= $_[1] },
"always-active" => sub { $alwaysActive //= $_[1] },
"pubKey=s" => \@pubKeys,
"comment=s" => sub { $comment //= $_[1] },
'uid-auto' => sub { $uidAuto //= $_[1] },
'osh-only' => sub { $oshOnly //= $_[1] },
'immutable-key' => sub { $immutableKey //= $_[1] },
'ttl=i' => sub { $ttl //= $_[1] },
);
};
if ($@) { die $@ }
if (!$result) {
local $" = ", ";
HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns");
}
if (!$account || !$type) {
HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'account' or 'type'");
}
#<HEADER
#>PARAMS:TYPE
osh_debug("Checking type");
if (not grep { $type eq $_ } qw{ normal realm }) {
HEXIT('ERR_INVALID_PARAMETER', "Expected type 'normal' or 'realm'");
}
#<PARAMS:TYPE
#>PARAMS:ACCOUNT
osh_debug("Checking account");
$fnret = OVH::Bastion::is_account_valid(account => $account);
$fnret or HEXIT($fnret);
# get returned untainted value
$account = $fnret->value->{'account'};
$fnret = OVH::Bastion::is_account_existing(account => $account);
$fnret->is_err and HEXIT($fnret);
$fnret->is_ok and HEXIT('KO_ALREADY_EXISTING', msg => "The account $account already exists");
$fnret = OVH::Bastion::is_group_existing(group => $account);
$fnret->is_err and HEXIT($fnret);
$fnret->is_ok and HEXIT('KO_ALREADY_EXISTING', msg => "The group $account already exists");
if ($type eq 'realm') {
$account = "realm_$account";
$fnret = OVH::Bastion::is_account_valid(account => $account, accountType => "realm");
$fnret or HEXIT($fnret);
$fnret = OVH::Bastion::is_account_existing(account => $account, accountType => "realm");
$fnret->is_err and HEXIT($fnret);
$fnret->is_ok and HEXIT('KO_ALREADY_EXISTING', msg => "The realm $account already exists");
$fnret = OVH::Bastion::is_group_existing(group => $account);
$fnret->is_err and HEXIT($fnret);
$fnret->is_ok and HEXIT('KO_ALREADY_EXISTING', msg => "The group $account already exists");
}
#<PARAMS:ACCOUNT
#>PARAMS:UID
if (not $uidAuto and not defined $uid) {
HEXIT('ERR_MISSING_PARAMETER', msg => "Missing one of 'uid-auto' or 'uid' argument");
}
if (defined $uid and $uidAuto) {
HEXIT('ERR_INCOMPATIBLE_PARAMETERS', msg => "Incompatible parameters 'uid' and 'uid-auto' specified");
}
if (defined $uid) {
$fnret = OVH::Bastion::is_valid_uid(uid => $uid, type => 'user');
$fnret or HEXIT($fnret);
$uid = $fnret->value;
getpwuid($uid) and HEXIT('ERR_UID_COLLISION', msg => "This UID ($uid) is already taken");
$fnret = OVH::Bastion::is_valid_uid(uid => $uid, type => 'group');
$fnret or HEXIT($fnret);
getgrgid($uid) and HEXIT('ERR_GID_COLLISION', msg => "This GID ($uid) is already taken");
}
elsif ($uidAuto) {
$fnret = OVH::Bastion::get_next_available_uid();
$fnret or HEXIT($fnret);
$uid = $fnret->value();
}
#<PARAMS:UID
#>PARAMS
my $ttygroup = "$account-tty";
$fnret = OVH::Bastion::is_group_existing(group => $ttygroup);
$fnret and HEXIT('ERR_TTY_GROUP_ALREADY_EXISTS', msg => "The TTY group for this account ($ttygroup) already exists!");
#<PARAMS
#>RIGHTSCHECK
if ($self eq 'root') {
osh_debug "Real root, skipping checks of permissions";
}
else {
# need to perform another security check
if ($type eq 'realm') {
$fnret = OVH::Bastion::is_user_in_group(user => $self, group => "osh-realmCreate");
$fnret or HEXIT('ERR_SECURITY_VIOLATION', msg => "You're not allowed to run this, dear $self");
}
else {
$fnret = OVH::Bastion::is_user_in_group(user => $self, group => "osh-accountCreate");
$fnret or HEXIT('ERR_SECURITY_VIOLATION', msg => "You're not allowed to run this, dear $self");
}
}
#<RIGHTSCHECK
#>CODE
$fnret = OVH::Bastion::load_configuration();
$fnret or HEXIT($fnret);
my $config = $fnret->value;
my $ttygid = $uid + $config->{'ttyrecGroupIdOffset'};
getgrgid($ttygid) and HEXIT('ERR_GID_COLLISION', msg => "This GID ($ttygid) is already taken");
if ($uid < $config->{'accountUidMin'} or $uid > $config->{'accountUidMax'}) {
HEXIT('ERR_UID_INVALID_RANGE', msg => "UID must be < " . $config->{'accountUidMin'} . " and > " . $config->{'accountUidMax'});
}
my @vettedKeys;
foreach my $key (@pubKeys) {
$fnret = OVH::Bastion::is_valid_public_key(pubKey => $key, way => 'ingress');
$fnret or HEXIT($fnret);
$key = $fnret->value->{'typecode'} . ' ' . $fnret->value->{'base64'};
if ($fnret->value->{'comment'}) {
$key .= ' ' . $fnret->value->{'comment'};
}
push @vettedKeys, $key;
}
my $prefix = $fnret->value->{'prefix'};
my @userProvidedIpList = ();
if ($prefix) {
my ($fromString) = $prefix =~ m{from=["']([^"']+)["']};
if ($fromString) {
@userProvidedIpList = split /,/, $fromString;
}
}
$fnret = OVH::Bastion::get_from_for_user_key(userProvidedIpList => \@userProvidedIpList);
$fnret or HEXIT($fnret);
my $from = $fnret->value->{'from'};
my $ipList = $fnret->value->{'ipList'};
my $homedir = "/home/$account";
osh_info "Creating group $account with GID $uid...";
$fnret = OVH::Bastion::sys_groupadd(noisy_stderr => 1, gid => $uid, group => $account);
$fnret->err eq 'OK'
or HEXIT('ERR_GROUPADD_FAILED', msg => "Error while running groupadd with UID $uid and group $account (" . $fnret->msg . ")");
osh_debug('ok, group was created');
osh_info "Creating user $account with UID $uid...";
$fnret = OVH::Bastion::sys_useradd(
noisy_stderr => 1,
user => $account,
uid => $uid,
gid => $uid,
shell => $OVH::Bastion::BASEPATH . '/bin/shell/osh.pl',
home => $homedir
);
$fnret->err eq 'OK'
or HEXIT('ERR_USERADD_FAILED', msg => "Error while running useradd for $account UID/GID $uid (" . $fnret->msg . ")");
osh_debug('user created');
chmod 0750, $homedir;
mkdir $homedir . "/.ssh" if (!-d "$homedir/.ssh");
chmod 0750, $homedir . "/.ssh";
chown $uid, $uid, "$homedir/.ssh";
if (!OVH::Bastion::touch_file("$homedir/.ssh/authorized_keys2")) {
HEXIT('ERR_CANNOT_CREATE_FILE', msg => "Failed to create authorized_keys file");
}
chmod 0640, $homedir . "/.ssh/authorized_keys2";
chown $uid, $uid, "$homedir/.ssh/authorized_keys2";
osh_info "Creating tty group of account...";
$fnret = OVH::Bastion::sys_groupadd(noisy_stderr => 1, group => $ttygroup, gid => $ttygid);
$fnret->err eq 'OK'
or HEXIT('ERR_GROUPADD_FAILED', msg => "Error while running groupadd with UID $ttygid and group $ttygroup (" . $fnret->msg . ")");
osh_debug('ok, group was created');
$fnret = OVH::Bastion::add_user_to_group(user => $account, group => $ttygroup, groupType => 'tty', accountType => ($type eq 'realm' ? 'realm' : 'normal'));
$fnret or HEXIT($fnret);
# adding account to bastion-users group
$fnret = OVH::Bastion::add_user_to_group(user => $account, group => "bastion-users", accountType => ($type eq 'realm' ? 'realm' : 'normal'));
$fnret or HEXIT($fnret);
if ($type ne 'realm') {
osh_info "Adding account to potential supplementary groups...";
if ($config->{'accountCreateSupplementaryGroups'}) {
foreach my $suppGroup (@{$config->{'accountCreateSupplementaryGroups'}}) {
$fnret = OVH::Bastion::add_user_to_group(user => $account, group => $suppGroup, groupType => 'osh');
if ($fnret) {
osh_info "Account added to group $suppGroup";
}
else {
osh_warn "Couldn't add account $account to group $suppGroup";
}
}
}
}
osh_info "Creating needed files and directories with proper permissions in home...";
my $ttyrecdir = $homedir . "/ttyrec";
mkdir $ttyrecdir;
if (!chown $uid, $uid, $ttyrecdir) {
HEXIT('ERR_CANNOT_CHOWN', msg => "Couldn't chown ttyrec directory ($!)");
}
if (!chmod 0700, $ttyrecdir) {
HEXIT('ERR_CANNOT_CHMOD', msg => "Couldn't chmod ttyrec directory ($!)");
}
osh_debug('applying an acl for group ' . $ttygroup);
OVH::Bastion::sys_setfacl(target => $ttyrecdir, clear => 1, perms => "g:$ttygroup:rX")
or HEXIT('ERR_SETFACL_FAILED', msg => "Error setting ACL on $ttyrecdir");
OVH::Bastion::sys_setfacl(target => $ttyrecdir, default => 1, perms => "g:$ttygroup:rX")
or HEXIT('ERR_SETFACL_FAILED', msg => "Error setting default ACL on $ttyrecdir");
OVH::Bastion::sys_setfacl(target => $homedir, clear => 1, perms => "g:$ttygroup:x,g:osh-auditor:x")
or HEXIT('ERR_SETFACL_FAILED', msg => "Error setting ACL on $homedir");
OVH::Bastion::sys_setfacl(target => "$homedir/.ssh", clear => 1, perms => "g:osh-auditor:x")
or HEXIT('ERR_SETFACL_FAILED', msg => "Error setting ACL on $homedir/.ssh");
osh_info "Creating some more directories...";
mkdir "/home/allowkeeper/$account";
OVH::Bastion::touch_file("/home/allowkeeper/$account/allowed.ip");
OVH::Bastion::touch_file("/home/allowkeeper/$account/allowed.private");
osh_info "Applying proper ownerships...";
$fnret = OVH::Bastion::execute(
cmd => ['chown', 'allowkeeper:allowkeeper', "/home/allowkeeper/$account", "/home/allowkeeper/$account/allowed.ip", "/home/allowkeeper/$account/allowed.private"],
noisy_stderr => 1
);
$fnret->err eq 'OK' or HEXIT('ERR_CHMOD_FAILED', msg => "Error while running chmod on allowkeeper (" . $fnret->msg . ")");
$fnret = OVH::Bastion::execute(cmd => ['chmod', '-R', 'o+rX', "/home/allowkeeper/$account"], noisy_stderr => 1);
$fnret->err eq 'OK' or HEXIT('ERR_CHMOD_FAILED', msg => "Error while running chmod -R on allowkeeper (" . $fnret->msg . ")");
if (ref $config->{'accountCreateDefaultPersonalAccesses'} eq 'ARRAY' && $type eq 'normal') {
foreach my $defAccess (@{$config->{'accountCreateDefaultPersonalAccesses'}}) {
my (undef, $user, $ip, undef, $port) = $defAccess =~ m{^(([^@]+)@)?([0-9./]+)(:(\d+))?$};
next unless $ip;
my @command = qw{ sudo -n -u allowkeeper -- };
push @command, $OVH::Bastion::BASEPATH . '/bin/helper/osh-accountModifyPersonalAccess';
push @command, '--target', 'any';
push @command, '--action', 'add';
push @command, '--account', $account;
push @command, '--ip', $ip;
if ($user) {
push @command, '--user', ($user eq 'ACCOUNT' ? $account : $user);
}
$port and push @command, '--port', $port;
$fnret = OVH::Bastion::execute(cmd => \@command, noisy_stdout => 1, noisy_stderr => 1, is_helper => 1);
$fnret->err eq 'OK' or osh_warn("Couldn't add private access to account to $defAccess (" . $fnret->msg . ")");
}
}
if (not defined $comment) {
$comment = '(no_comment_provided)';
}
$comment = "CREATED_BY=$self\nBASTION_VERSION=" . $OVH::Bastion::VERSION . "\nCREATION_TIME=" . localtime() . "\nCREATION_TIMESTAMP=" . time() . "\nCOMMENT=" . $comment . "\n";
if (open(my $fh_comment, '>>', $homedir . '/accountCreate.comment')) {
print $fh_comment $comment;
close $fh_comment;
chmod 0644, $homedir . '/accountCreate.comment';
}
$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");
}
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");
}
}
if ($alwaysActive || $type eq 'realm') {
$fnret = OVH::Bastion::account_config(account => $account, key => OVH::Bastion::OPT_ACCOUNT_ALWAYS_ACTIVE, value => "yes", public => 1);
if (!$fnret) {
osh_warn("Couldn't store always_active flag (" . $fnret->msg . "), continuing anyway");
}
}
$fnret = OVH::Bastion::add_user_to_group(user => "keyreader", group => $account, accountType => 'group');
$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...";
my $allowedKeyFile = $homedir . '/.ssh/authorized_keys2';
if (open(my $fh_keys, '>>', $allowedKeyFile)) {
foreach my $key (@vettedKeys) {
print $fh_keys $finalPrefix . $key . "\n";
}
close($fh_keys);
}
else {
HEXIT("ERR_CANNOT_ADD_KEY", msg => "Couldn't open $allowedKeyFile when trying to add provided public key");
}
# push this flag to prevent ssh/telnet usage
if ($oshOnly) {
$fnret = OVH::Bastion::account_config(account => $account, key => "osh_only", value => "yes");
$fnret or HEXIT($fnret);
}
# chown to root so user can no longer touch it
if ($immutableKey) {
chown 0, -1, $allowedKeyFile;
}
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");
my $sudoers_dir = OVH::Bastion::sys_getsudoersfolder();
if (-e "$sudoers_dir/osh-account-$account") {
osh_debug "sudoers already in place, but overwriting it";
}
$fnret = OVH::Bastion::execute(cmd => [$OVH::Bastion::BASEPATH . '/bin/sudogen/generate-sudoers.sh', 'account', $account], must_succeed => 1, noisy_stdout => 1);
$fnret or HEXIT('ERR_CANNOT_CREATE_SUDOERS', msg => "An error occurred while creating sudoers for this account");
my $bastionName = $config->{'bastionName'};
my $bastionCommand = $config->{'bastionCommand'};
$bastionCommand =~ s/USER|ACCOUNT/$account/g;
$bastionCommand =~ s/CACHENAME|BASTIONNAME/$bastionName/g;
my $hostname = Sys::Hostname::hostname();
$bastionCommand =~ s/HOSTNAME/$hostname/g;
if ($type eq 'realm') {
osh_info "Realm has been created.";
}
else {
osh_info "==> alias $bastionName='$bastionCommand'";
osh_info "To test his access, ask this user to set the above alias in his .bash_aliases, then run `$bastionName --osh info'";
}
OVH::Bastion::syslogFormatted(
severity => 'info',
type => 'account',
fields => [
['action', 'create'],
['account', $account],
['uid', $uid],
['public_key', @vettedKeys ? $vettedKeys[0] : undef],
['always_active', ($alwaysActive ? 'true' : 'false')],
['uid_auto', ($uidAuto ? 'true' : 'false')],
['osh_only', ($oshOnly ? 'true' : 'false')],
['immutable_key', ($immutableKey ? 'true' : 'false')],
['comment', $comment],
]
);
HEXIT('OK');

218
bin/helper/osh-accountDelete Executable file
View file

@ -0,0 +1,218 @@
#! /usr/bin/perl -T
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
# NEEDGROUP osh-accountDelete
# SUDOERS %osh-accountDelete ALL=(root) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountDelete *
# FILEMODE 0700
# FILEOWN root root
#>HEADER
use common::sense;
use Getopt::Long;
use File::Copy qw(move);
use File::Basename;
use lib dirname(__FILE__) . '/../../lib/perl';
use OVH::Bastion;
local $| = 1;
#
# Globals
#
$SIG{'HUP'} = 'IGNORE'; # continue even when attached terminal is closed (we're called with setsid on supported systems anyway)
$SIG{'PIPE'} = 'IGNORE'; # continue even if osh_info gets a SIGPIPE because there's no longer a terminal
$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/pkg/bin';
my ($self) = $ENV{'SUDO_USER'} =~ m{^([a-zA-Z0-9._-]+)$};
if (not defined $self) {
if ($< == 0) {
$self = 'root';
}
else {
HEXIT('ERR_SUDO_NEEDED', msg => 'This command must be run under sudo');
}
}
# Fetch command options
my $fnret;
my ($result, @optwarns);
my ($account, $type);
eval {
local $SIG{__WARN__} = sub { push @optwarns, shift };
$result = GetOptions(
"account=s" => sub { $account //= $_[1] },
"type=s" => sub { $type //= $_[1] },
);
};
if ($@) { die $@ }
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
#>RIGHTSCHECK
if ($self eq 'root') {
osh_debug "Real root, skipping checks of permissions";
}
else {
# need to perform another security check
$fnret = OVH::Bastion::is_user_in_group(user => $self, group => "osh-accountDelete");
if (!$fnret) {
HEXIT('ERR_SECURITY_VIOLATION', msg => "You're not allowed to run this, dear $self");
}
}
#<RIGHTSCHECK
#>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_bastion_account_valid_and_existing(account => $account, accountType => $type);
$fnret or HEXIT($fnret);
# get returned untainted value
$account = $fnret->value->{'account'};
#>CODE
# don't allow a non-admin deleting an admin
if (OVH::Bastion::is_admin(account => $account, sudo => 1) && !OVH::Bastion::is_admin(account => $self, sudo => 1)) {
HEXIT('ERR_SECURITY_VIOLATION', msg => "You can't delete an admin without being admin yourself");
}
# Try to find the user -tty group
my $ttygroup = "$account-tty";
if (!getgrnam($ttygroup)) {
$ttygroup = substr($account, 0, 5) . '-tty';
if (!getgrnam($ttygroup)) {
osh_warn("Couldn't find out which is the tty group of this account, will still delete it anyway");
$ttygroup = undef;
}
}
# last security check
if ((getpwnam($account))[8] !~ m{/osh\.pl$}) {
HEXIT('ERR_INVALID_ACCOUNT', msg => "Account $account doesn't seem to be a legit bastion account");
}
# kill all user processes, if any
# GNU and BSD compliant
$fnret = OVH::Bastion::execute(cmd => ['ps', '-U', $account, '-o', 'pid'], noisy_stderr => 1);
if ($fnret->err ne 'OK' || ref $fnret->value->{'stdout'} ne 'ARRAY') {
; # don't warn, we can get an return code of 1 just because there are no processes matching
}
else {
# don't check kill return because it may fail if the process died since,
# and it's not a big issue, we'll still delete the account
# we have to untaint what `ps` gave us however
my @pids;
foreach my $pid (@{$fnret->value->{'stdout'}}) {
push @pids, $1 if ($pid =~ m{(\d+)});
}
kill 'KILL', @pids if @pids;
}
# do the stuff
if (!-d "/home/oldkeeper") {
mkdir "/home/oldkeeper";
}
chown 0, 0, "/home/oldkeeper";
chmod 0700, "/home/oldkeeper";
if (!-d "/home/oldkeeper/accounts") {
mkdir "/home/oldkeeper/accounts";
}
chown 0, 0, "/home/oldkeeper/accounts";
chmod 0700, "/home/oldkeeper/accounts";
my $suffix = 'at-' . time() . '.by-' . $self;
my $fulldir = "/home/oldkeeper/accounts/$account.$suffix";
if (-e $fulldir) {
HEXIT('ERR_BACKUP_DIR_COLLISION', msg => "This shouldn't happen, $fulldir already exists!");
}
mkdir $fulldir;
chown 0, 0, $fulldir;
chmod 0700, $fulldir;
move("/home/$account", "$fulldir/$account-home");
move("/home/allowkeeper/$account", "$fulldir/allowkeeper");
# remove +a or tar won't be able to rm files, don't check if it succeeded if we're on a system without chattr
$fnret = OVH::Bastion::execute(cmd => ['find', "$fulldir/$account-home", '-maxdepth', '1', '-name', "*.log", '-exec', 'chattr', '-a', '{}', ';']);
# remove sudoers if it's there
unlink(OVH::Bastion::sys_getsudoersfolder() . "/osh-account-$account");
# add a text file with all the groups the user was a member of
$fnret = OVH::Bastion::get_user_groups(account => $account, extra => 1);
if ($fnret) {
if (open(my $txtfile, '>', "$fulldir/groups.txt")) {
print $txtfile join("\n", @{$fnret->value});
close($txtfile);
}
else {
osh_warn("Couldn't open the groups.txt file to save the group list of this account ($!)");
}
}
# now tar.gz the directory, this is important because inside we'll keep the
# old UID of the user, and we don't want UID-orphaned on our filesystem, it's
# not a problem to have those inside a tarfile however.
my @tarcmd = qw{ tar czf };
push @tarcmd, $fulldir . '.tar.gz';
push @tarcmd, '--acls' if OVH::Bastion::has_acls();
push @tarcmd, '--one-file-system', '-p', '--remove-files', $fulldir;
osh_info("Backing up home directory...");
$fnret = OVH::Bastion::execute(cmd => \@tarcmd, must_succeed => 1);
if (!$fnret) {
osh_warn("Couldn't tar the backup homedir of this account (" . $fnret->msg . "), proceeding anyway.");
chmod 0000, $fulldir;
}
else {
chmod 0000, $fulldir . '.tar.gz';
unlink($fulldir);
}
osh_info("Backup done");
osh_info "Removing '$account' group membership from 'keyreader' user";
$fnret = OVH::Bastion::sys_delmemberfromgroup(user => "keyreader", group => $account);
$fnret or HEXIT($fnret);
osh_info "Deleting system user '$account'...";
$fnret = OVH::Bastion::sys_userdel(user => $account);
$fnret or HEXIT($fnret);
# some systems don't delete the primary group with userdel (suse at least)
$fnret = OVH::Bastion::execute(cmd => ['getent', 'group', $account]);
if ($fnret && $fnret->value->{'sysret'} == 0) {
osh_info "Deleting account main group '$account'...";
$fnret = OVH::Bastion::sys_groupdel(group => $account);
$fnret or HEXIT($fnret);
}
if (defined $ttygroup) {
osh_info "Deleting group $ttygroup...";
$fnret = OVH::Bastion::sys_groupdel(group => $ttygroup);
$fnret or HEXIT($fnret);
}
OVH::Bastion::syslogFormatted(
severity => 'info',
type => 'account',
fields => [['action', 'delete'], ['account', $account], ['tty_group', $ttygroup],]
);
HEXIT('OK', value => {account => $account, ttygroup => $ttygroup, operation => 'deleted'});

View file

@ -0,0 +1,56 @@
#! /usr/bin/perl -T
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
# SUDOERS # to be able to generate an egress password for accounts
# SUDOERS %osh-accountGeneratePassword ALL=(%bastion-users) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountGeneratePassword *
# FILEMODE 0755
# FILEOWN root root
#>HEADER
use common::sense;
use Getopt::Long;
use File::Basename;
use lib dirname(__FILE__) . '/../../lib/perl';
use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin::generatePassword;
local $| = 1;
#
# Globals
#
$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/pkg/bin';
my ($self) = $ENV{'SUDO_USER'} =~ m{^([a-zA-Z0-9._-]+)$};
if (not defined $self) {
if ($< == 0) {
$self = 'root';
}
else {
HEXIT('ERR_SUDO_NEEDED', msg => 'This command must be run under sudo');
}
}
# Fetch command options
my ($result, @optwarns);
my ($account, $size);
eval {
local $SIG{__WARN__} = sub { push @optwarns, shift };
$result = GetOptions(
"account=s" => sub { $account //= $_[1] },
"size=i" => sub { $size //= $_[1] },
);
};
if ($@) { die $@ }
if (!$result) {
local $" = ", ";
HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns");
}
if (not $size or not $account) {
HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'size' or 'account'");
}
#<HEADER
HEXIT(OVH::Bastion::Plugin::generatePassword::act(self => $self, context => 'account', account => $account, size => $size, sudo => 1));

View file

@ -0,0 +1,91 @@
#! /usr/bin/perl -T
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
# NEEDGROUP osh-auditor
# SUDOERS %osh-auditor ALL=(root) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountGetPasswordInfo *
# FILEMODE 0700
# FILEOWN root root
#>HEADER
use common::sense;
use Getopt::Long;
use Sys::Hostname ();
use File::Basename;
use lib dirname(__FILE__) . '/../../lib/perl';
use OVH::Bastion;
local $| = 1;
#
# Globals
#
$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/pkg/bin';
my ($self) = $ENV{'SUDO_USER'} =~ m{^([a-zA-Z0-9._-]+)$};
if (not defined $self) {
if ($< == 0) {
$self = 'root';
}
else {
HEXIT('ERR_SUDO_NEEDED', msg => 'This command must be run under sudo');
}
}
# Fetch command options
my $fnret;
my ($result, @optwarns);
my ($account, $all);
eval {
local $SIG{__WARN__} = sub { push @optwarns, shift };
$result = GetOptions(
"account=s" => sub { $account //= $_[1] },
"all" => sub { $all //= $_[1] },
);
};
if ($@) { die $@ }
if (!$result) {
local $" = ", ";
HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns");
}
if (!$account && !$all) {
HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'account' or 'all'");
}
#<HEADER
#>PARAMS:ACCOUNT
if ($account) {
osh_debug("Checking account");
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account);
$fnret or HEXIT($fnret);
# get returned untainted value
$account = $fnret->value->{'account'};
}
#<PARAMS:ACCOUNT
#>RIGHTSCHECK
if ($self eq 'root') {
osh_debug "Real root, skipping checks of permissions";
}
else {
$fnret = OVH::Bastion::is_user_in_group(user => $self, group => "osh-auditor");
$fnret or HEXIT('ERR_SECURITY_VIOLATION', msg => "You're not allowed to run this, dear $self");
}
#<RIGHTSCHECK
#>CODE
if ($account) {
HEXIT(OVH::Bastion::sys_getpasswordinfo(user => $account));
}
$fnret = OVH::Bastion::get_account_list();
$fnret or HEXIT($fnret);
my %ret;
foreach my $acc (keys %{$fnret->value}) {
$ret{$acc} = OVH::Bastion::sys_getpasswordinfo(user => $acc)->value;
$ret{$acc}{'name'} = $acc;
}
HEXIT('OK', value => \%ret);

View file

@ -0,0 +1,74 @@
#! /usr/bin/perl -T
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
# NEEDGROUP osh-accountListEgressKeys
# SUDOERS %osh-accountListEgressKeys ALL=(keyreader) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountListEgressKeys *
# FILEMODE 0750
# FILEOWN root keyreader
#>HEADER
use common::sense;
use Getopt::Long;
use File::Basename;
use lib dirname(__FILE__) . '/../../lib/perl';
use OVH::Bastion;
local $| = 1;
#
# Globals
#
$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/pkg/bin';
my ($self) = $ENV{'SUDO_USER'} =~ m{^([a-zA-Z0-9._-]+)$};
if (not defined $self) {
if ($< == 0) {
$self = 'root';
}
else {
HEXIT('ERR_SUDO_NEEDED', msg => 'This command must be run under sudo');
}
}
# Fetch command options
my $fnret;
my ($result, @optwarns);
my ($account);
eval {
local $SIG{__WARN__} = sub { push @optwarns, shift };
$result = GetOptions("account=s" => sub { $account //= $_[1] });
};
if ($@) { die $@ }
if (!$result) {
local $" = ", ";
HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns");
}
if (!$account) {
HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'account'");
}
#<HEADER
#>RIGHTSCHECK
if ($self eq 'root') {
osh_debug "Real root, skipping checks of permissions";
}
else {
# need to perform another security check
$fnret = OVH::Bastion::is_user_in_group(user => $self, group => "osh-accountListEgressKeys");
if (!$fnret) {
HEXIT('ERR_SECURITY_VIOLATION', msg => "You're not allowed to run this, dear $self");
}
}
#<RIGHTSCHECK
#>PARAMS:ACCOUNT
osh_debug("Checking account");
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account);
$fnret or HEXIT($fnret);
$account = $fnret->value->{'account'}; # untainted
#<PARAMS:ACCOUNT
HEXIT(OVH::Bastion::get_personal_account_keys(account => $account));

View file

@ -0,0 +1,87 @@
#! /usr/bin/perl -T
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
# NEEDGROUP osh-accountListIngressKeys
# SUDOERS %osh-accountListIngressKeys ALL=(keyreader) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountListIngressKeys *
# FILEMODE 0750
# FILEOWN root keyreader
#>HEADER
use common::sense;
use Getopt::Long;
use File::Basename;
use lib dirname(__FILE__) . '/../../lib/perl';
use OVH::Bastion;
local $| = 1;
#
# Globals
#
$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/pkg/bin';
my ($self) = $ENV{'SUDO_USER'} =~ m{^([a-zA-Z0-9._-]+)$};
if (not defined $self) {
if ($< == 0) {
$self = 'root';
}
else {
HEXIT('ERR_SUDO_NEEDED', msg => 'This command must be run under sudo');
}
}
# Fetch command options
my $fnret;
my ($result, @optwarns);
my ($account);
eval {
local $SIG{__WARN__} = sub { push @optwarns, shift };
$result = GetOptions("account=s" => sub { $account //= $_[1] });
};
if ($@) { die $@ }
if (!$result) {
local $" = ", ";
HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns");
}
if (!$account) {
HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'account'");
}
#<HEADER
#>RIGHTSCHECK
if ($self eq 'root') {
osh_debug "Real root, skipping checks of permissions";
}
else {
# need to perform another security check
$fnret = OVH::Bastion::is_user_in_group(user => $self, group => ($account eq 'root' ? "osh-rootListIngressKeys" : "osh-accountListIngressKeys"));
if (!$fnret) {
HEXIT('ERR_SECURITY_VIOLATION', msg => "You're not allowed to run this, dear $self");
}
}
#<RIGHTSCHECK
#>PARAMS:ACCOUNT
osh_debug("Checking account");
my $accounthome;
if ($account ne 'root') {
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account);
$fnret or HEXIT($fnret);
$account = $fnret->value->{'account'}; # untainted
$accounthome = $fnret->value->{'dir'};
}
else {
$account = 'root';
$accounthome = '/root';
}
#<PARAMS:ACCOUNT
my @keys;
foreach my $file ("$accounthome/.ssh/authorized_keys2", "$accounthome/.ssh/authorized_keys") {
$fnret = OVH::Bastion::get_authorized_keys_from_file(file => $file);
push @keys, @{$fnret->value} if ($fnret && $fnret->value);
}
HEXIT('OK', value => \@keys);

View file

@ -0,0 +1,74 @@
#! /usr/bin/perl -T
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
# NEEDGROUP osh-accountListPasswords
# SUDOERS %osh-accountListPasswords ALL=(%bastion-users) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountListPasswords *
# FILEMODE 0755
# FILEOWN root root
#>HEADER
use common::sense;
use Getopt::Long;
use File::Basename;
use lib dirname(__FILE__) . '/../../lib/perl';
use OVH::Bastion;
local $| = 1;
#
# Globals
#
$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/pkg/bin';
my ($self) = $ENV{'SUDO_USER'} =~ m{^([a-zA-Z0-9._-]+)$};
if (not defined $self) {
if ($< == 0) {
$self = 'root';
}
else {
HEXIT('ERR_SUDO_NEEDED', msg => 'This command must be run under sudo');
}
}
# Fetch command options
my $fnret;
my ($result, @optwarns);
my ($account);
eval {
local $SIG{__WARN__} = sub { push @optwarns, shift };
$result = GetOptions("account=s" => sub { $account //= $_[1] });
};
if ($@) { die $@ }
if (!$result) {
local $" = ", ";
HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns");
}
if (!$account) {
HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'account'");
}
#<HEADER
#>RIGHTSCHECK
if ($self eq 'root') {
osh_debug "Real root, skipping checks of permissions";
}
else {
# need to perform another security check
$fnret = OVH::Bastion::is_user_in_group(user => $self, group => "osh-accountListPasswords");
if (!$fnret) {
HEXIT('ERR_SECURITY_VIOLATION', msg => "You're not allowed to run this, dear $self");
}
}
#<RIGHTSCHECK
#>PARAMS:ACCOUNT
osh_debug("Checking account");
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account);
$fnret or HEXIT($fnret);
$account = $fnret->value->{'account'}; # untainted
#<PARAMS:ACCOUNT
HEXIT(OVH::Bastion::get_hashes_list(context => 'account', account => $account));

View file

@ -0,0 +1,94 @@
#! /usr/bin/perl -T
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
# NEEDGROUP osh-accountMFAResetPassword
# SUDOERS %osh-accountMFAResetPassword ALL=(root) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountMFAResetPassword --account *
# FILEMODE 0700
# FILEOWN root root
#>HEADER
use common::sense;
use Getopt::Long;
use File::Basename;
use lib dirname(__FILE__) . '/../../lib/perl';
use OVH::Result;
use OVH::Bastion;
local $| = 1;
#
# Globals
#
$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/pkg/bin';
my ($self) = $ENV{'SUDO_USER'} =~ m{^([a-zA-Z0-9._-]+)$};
if (not defined $self) {
if ($< == 0) {
$self = 'root';
}
else {
HEXIT('ERR_SUDO_NEEDED', msg => 'This command must be run under sudo');
}
}
# Fetch command options
my $fnret;
my ($result, @optwarns);
my ($account);
eval {
local $SIG{__WARN__} = sub { push @optwarns, shift };
$result = GetOptions("account=s" => sub { $account //= $_[1] },);
};
if ($@) { die $@ }
if (!$result) {
local $" = ", ";
HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns");
}
if (not $account) {
HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'account'");
}
#>PARAMS:ACCOUNT
osh_debug("Checking account");
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account);
$fnret or HEXIT($fnret);
$account = $fnret->value->{'account'}; # untainted
#<PARAMS:ACCOUNT
#>RIGHTSCHECK
if ($self eq 'root') {
osh_debug "Real root, skipping checks of permissions";
}
# special case for self: if account==self, then is ok
elsif ($self ne $account) {
$fnret = OVH::Bastion::is_user_in_group(user => $self, group => "osh-accountMFAResetPassword");
if (!$fnret) {
HEXIT('ERR_SECURITY_VIOLATION', msg => "You're not allowed to run this, dear $self");
}
}
#<RIGHTSCHECK
# don't allow a non-admin to reset the Password of an admin
if (OVH::Bastion::is_admin(account => $account, sudo => 1) && !OVH::Bastion::is_admin(account => $self, sudo => 1)) {
HEXIT('ERR_SECURITY_VIOLATION', msg => "You can't reset the password of an admin without being admin yourself");
}
if (OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::MFA_PASSWORD_CONFIGURED_GROUP)) {
$fnret = OVH::Bastion::sys_delmemberfromgroup(user => $account, group => OVH::Bastion::MFA_PASSWORD_CONFIGURED_GROUP);
$fnret or HEXIT($fnret);
}
$fnret = OVH::Bastion::sys_neutralizepassword(user => $account);
$fnret or HEXIT($fnret);
# remove expiration, or user could get locked out if s/he doesn't quickly set a new password,
# as the password expiration time is still taken into account even for '*' passwords
# 99999 is the /etc/shadow way to say "never" (273 years)
$fnret = OVH::Bastion::sys_setpasswordpolicy(user => $account, maxDays => 99999);
$fnret or HEXIT($fnret);
osh_info "Password has been reset, " . ($account eq $self ? 'you' : $account) . " can setup a new password by using the `--osh selfMFASetupPassword' command, if applicable";
HEXIT('OK');

View file

@ -0,0 +1,91 @@
#! /usr/bin/perl -T
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
# NEEDGROUP osh-accountMFAResetTOTP
# SUDOERS %osh-accountMFAResetTOTP ALL=(root) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountMFAResetTOTP --account *
# FILEMODE 0700
# FILEOWN root root
#>HEADER
use common::sense;
use Getopt::Long;
use File::Basename;
use lib dirname(__FILE__) . '/../../lib/perl';
use OVH::Result;
use OVH::Bastion;
local $| = 1;
#
# Globals
#
$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/pkg/bin';
my ($self) = $ENV{'SUDO_USER'} =~ m{^([a-zA-Z0-9._-]+)$};
if (not defined $self) {
if ($< == 0) {
$self = 'root';
}
else {
HEXIT('ERR_SUDO_NEEDED', msg => 'This command must be run under sudo');
}
}
# Fetch command options
my $fnret;
my ($result, @optwarns);
my ($account);
eval {
local $SIG{__WARN__} = sub { push @optwarns, shift };
$result = GetOptions("account=s" => sub { $account //= $_[1] },);
};
if ($@) { die $@ }
if (!$result) {
local $" = ", ";
HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns");
}
if (not $account) {
HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'account'");
}
#>PARAMS:ACCOUNT
osh_debug("Checking account");
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account);
$fnret or HEXIT($fnret);
$account = $fnret->value->{'account'}; # untainted
my $home = $fnret->value->{'dir'};
#<PARAMS:ACCOUNT
#>RIGHTSCHECK
if ($self eq 'root') {
osh_debug "Real root, skipping checks of permissions";
}
# special case for self: if account==self, then is ok
elsif ($self ne $account) {
$fnret = OVH::Bastion::is_user_in_group(user => $self, group => "osh-accountMFAResetTOTP");
if (!$fnret) {
HEXIT('ERR_SECURITY_VIOLATION', msg => "You're not allowed to run this, dear $self");
}
}
#<RIGHTSCHECK
# don't allow a non-admin to reset the TOTP of an admin
if (OVH::Bastion::is_admin(account => $account, sudo => 1) && !OVH::Bastion::is_admin(account => $self, sudo => 1)) {
HEXIT('ERR_SECURITY_VIOLATION', msg => "You can't reset the TOTP of an admin without being admin yourself");
}
if (OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::MFA_TOTP_CONFIGURED_GROUP)) {
$fnret = OVH::Bastion::sys_delmemberfromgroup(user => $account, group => OVH::Bastion::MFA_TOTP_CONFIGURED_GROUP);
$fnret or HEXIT($fnret);
}
# remove the .otp file (non-fatal)
if (!unlink($home . '/' . OVH::Bastion::TOTP_FILENAME)) {
osh_warn("Couldn't remove the TOTP file ($!), this is not fatal, continuing anyway");
}
osh_info "TOTP has been reset, " . ($account eq $self ? 'you' : $account) . " can re-enroll by using the `--osh selfMFASetupTOTP' command, if applicable";
HEXIT('OK');

334
bin/helper/osh-accountModify Executable file
View file

@ -0,0 +1,334 @@
#! /usr/bin/perl -T
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
# NEEDGROUP osh-accountModify
# SUDOERS # modify parameters/policy of an account
# SUDOERS %osh-accountModify ALL=(root) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountModify *
# FILEMODE 0700
# FILEOWN root root
#>HEADER
use common::sense;
use Getopt::Long;
use File::Basename;
use lib dirname(__FILE__) . '/../../lib/perl';
use OVH::Bastion;
use OVH::Result;
local $| = 1;
#
# Globals
#
$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/pkg/bin';
my ($self) = $ENV{'SUDO_USER'} =~ m{^([a-zA-Z0-9._-]+)$};
if (not defined $self) {
if ($< == 0) {
$self = 'root';
}
else {
HEXIT('ERR_SUDO_NEEDED', msg => 'This command must be run under sudo');
}
}
# Fetch command options
Getopt::Long::Configure("no_auto_abbrev");
my $fnret;
my ($result, @optwarns);
my ($account, @modify);
eval {
local $SIG{__WARN__} = sub { push @optwarns, shift };
$result = GetOptions(
"account=s" => sub { $account //= $_[1] },
"modify=s" => \@modify,
);
};
if ($@) { die $@ }
if (!$result) {
local $" = ", ";
HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns");
}
if (!$account || !@modify) {
HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'account' or 'modify'");
}
#<HEADER
#>PARAMS:ACCOUNT
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account, localOnly => 1);
$fnret or HEXIT($fnret);
# get returned untainted value
$account = $fnret->value->{'account'};
#<PARAMS:ACCOUNT
#>RIGHTSCHECK
if ($self eq 'root') {
osh_debug "Real root, skipping checks of permissions";
}
$fnret = OVH::Bastion::is_user_in_group(user => $self, group => "osh-accountModify");
if (!$fnret) {
HEXIT('ERR_SECURITY_VIOLATION', msg => "You're not allowed to run this, dear $self");
}
if (OVH::Bastion::is_admin(account => $account, sudo => 1) && !OVH::Bastion::is_admin(account => $self, sudo => 1)) {
HEXIT('ERR_SECURITY_VIOLATION', msg => "You can't modify the account of an admin without being admin yourself");
}
#<RIGHTSCHECK
#>CODE
my %result;
# the TOTP and UNIX Password toggle codes are extremely similar, factorize it here
sub _mfa_toggle {
my ($key, $value, $mfaName, $mfaGroup, $mfaGroupBypass) = @_;
my $jsonkey = $key;
$jsonkey =~ s/-/_/g;
# if the value is != bypass, remove the account from the bypass group
if ($value ne 'bypass') {
if (OVH::Bastion::is_user_in_group(user => $account, group => $mfaGroupBypass)) {
$fnret = OVH::Bastion::sys_delmemberfromgroup(user => $account, group => $mfaGroupBypass, noisy_stderr => 1);
if (!$fnret) {
osh_warn "... error while removing the bypass option for this account";
$result{$jsonkey} = R('ERR_REMOVING_FROM_GROUP');
return;
}
}
}
# if the value is == bypass, remove the account from the required group
elsif ($value eq 'bypass') {
if (OVH::Bastion::is_user_in_group(user => $account, group => $mfaGroup)) {
$fnret = OVH::Bastion::sys_delmemberfromgroup(user => $account, group => $mfaGroup, noisy_stderr => 1);
if (!$fnret) {
osh_warn "... error while removing the required option for this account";
$result{$jsonkey} = R('ERR_REMOVING_FROM_GROUP');
return;
}
}
}
$fnret = OVH::Bastion::is_user_in_group(user => $account, group => $mfaGroup);
if ($value eq 'yes') {
osh_info "Enforcing multi-factor authentication of type $mfaName for this account...";
if ($fnret) {
osh_info "... no change was required";
$result{$jsonkey} = R('OK_NO_CHANGE');
return;
}
$fnret = OVH::Bastion::sys_addmembertogroup(user => $account, group => $mfaGroup, noisy_stderr => 1);
if (!$fnret) {
osh_warn "... error while setting the enforce option";
$result{$jsonkey} = R('ERR_ADDING_TO_GROUP');
return;
}
osh_info "... done, this account is now required to setup a password with --osh selfMFASetup$mfaName on the next connection, before being allowed to do anything else";
$result{$jsonkey} = R('OK');
}
elsif ($value eq 'no') {
osh_info "Removing multi-factor authentication of type $mfaName requirement for this account...";
if (!$fnret) {
osh_info "... no change was required";
$result{$jsonkey} = R('OK_NO_CHANGE');
return;
}
$fnret = OVH::Bastion::sys_delmemberfromgroup(user => $account, group => $mfaGroup, noisy_stderr => 1);
if (!$fnret) {
osh_warn "... error while setting the enforce option";
$result{$jsonkey} = R('ERR_REMOVING_FROM_GROUP');
return;
}
osh_info
"... done, this account is no longer required to setup a password, however if there's already a password configured, it'll still be required (if this is not expected, the password can be reset with --osh accountMFAResetPassword command)";
$result{$jsonkey} = R('OK');
}
elsif ($value eq 'bypass') {
osh_info "Bypassing multi-factor authentication of type $mfaName requirement for this account...";
$fnret = OVH::Bastion::is_user_in_group(user => $account, group => $mfaGroupBypass);
if ($fnret) {
osh_info "... no change was required";
$result{$jsonkey} = R('OK_NO_CHANGE');
return;
}
$fnret = OVH::Bastion::sys_addmembertogroup(user => $account, group => $mfaGroupBypass, noisy_stderr => 1);
if (!$fnret) {
osh_warn "... error while setting the enforce option";
$result{$jsonkey} = R('ERR_ADDING_TO_GROUP');
return;
}
osh_info
"... done, this account will no longer have to setup a password, even if this is enforced by the default global policy.\nHowever if there's already a password configured, it'll still be required (if this is not expected, the password can be reset with --osh accountMFAResetPassword command)";
$result{$jsonkey} = R('OK');
}
return;
}
sub _toggle_yes_no {
my %params = @_;
my $keyname = $params{'keyname'};
my $keyfile = $params{'keyfile'};
my $value = $params{'value'};
$fnret = OVH::Bastion::account_config(account => $account, public => 1, key => $keyfile);
if ($value eq 'yes') {
osh_info "Setting this account as $keyname...";
if ($fnret) {
osh_info "... no change was required";
return R('OK_NO_CHANGE');
}
$fnret = OVH::Bastion::account_config(account => $account, public => 1, key => $keyfile, value => 'yes');
if (!$fnret) {
osh_warn "... error while setting the option";
return R('ERR_OPTION_CHANGE_FAILED');
}
osh_info "... done, this account is now $keyname";
return R('OK');
}
elsif ($value eq 'no') {
osh_info "Removing the $keyname flag from this account...";
if (!$fnret) {
osh_info "... no change was required";
return R('OK_NO_CHANGE');
}
$fnret = OVH::Bastion::account_config(account => $account, public => 1, key => $keyfile, delete => 1);
if (!$fnret) {
osh_warn "... error while removing the option";
return R('ERR_OPTION_CHANGE_FAILED');
}
osh_info "... done, this account has no longer the $keyname flag set";
return R('OK');
}
else {
return R('ERR_INVALID_PARAMETER', msg => "Invalid value passed to $keyfile");
}
}
foreach my $tuple (@modify) {
my ($key, $value) = $tuple =~ /^([a-zA-Z0-9-]+)=([a-zA-Z0-9-]+)$/;
next if (!$key || !$value);
my $jsonkey = $key;
$jsonkey =~ s/-/_/g;
osh_debug "working on tuple key=$key value=$value";
if ($key eq 'always-active') {
$result{$jsonkey} = _toggle_yes_no(value => $value, keyfile => OVH::Bastion::OPT_ACCOUNT_ALWAYS_ACTIVE, keyname => 'always-active');
}
elsif ($key eq 'idle-ignore') {
$result{$jsonkey} = _toggle_yes_no(value => $value, keyfile => OVH::Bastion::OPT_ACCOUNT_IDLE_IGNORE, keyname => 'idle-ignore');
}
elsif ($key eq 'pam-auth-bypass') {
$fnret = OVH::Bastion::is_user_in_group(user => $account, group => OVH::Bastion::PAM_AUTH_BYPASS_GROUP);
if ($value eq 'yes') {
{
osh_info "Bypassing sshd PAM auth usage for this account...";
if ($fnret) {
osh_info "... no change was required";
$result{$jsonkey} = R('OK_NO_CHANGE');
last;
}
$fnret = OVH::Bastion::sys_addmembertogroup(user => $account, group => OVH::Bastion::PAM_AUTH_BYPASS_GROUP, noisy_stderr => 1);
if (!$fnret) {
osh_warn "... error while setting the bypass option";
$result{$jsonkey} = R('ERR_ADDING_TO_GROUP');
last;
}
osh_info "... done, this account will no longer use PAM for authentication";
$result{$jsonkey} = R('OK');
}
}
elsif ($value eq 'no') {
{
osh_info "Removing bypass of sshd PAM auth usage for this account...";
if (!$fnret) {
osh_info "... no change was required";
$result{$jsonkey} = R('OK_NO_CHANGE');
last;
}
$fnret = OVH::Bastion::sys_delmemberfromgroup(user => $account, group => OVH::Bastion::PAM_AUTH_BYPASS_GROUP, noisy_stderr => 1);
if (!$fnret) {
osh_warn "... error while removing the bypass option";
$result{$jsonkey} = R('ERR_REMOVING_FROM_GROUP');
last;
}
osh_info "... done, this account will no longer bypass PAM for authentication";
$result{$jsonkey} = R('OK');
}
}
}
elsif ($key eq 'mfa-password-required') {
_mfa_toggle($key, $value, 'Password', OVH::Bastion::MFA_PASSWORD_REQUIRED_GROUP, OVH::Bastion::MFA_PASSWORD_BYPASS_GROUP);
}
elsif ($key eq 'mfa-totp-required') {
_mfa_toggle($key, $value, 'TOTP', OVH::Bastion::MFA_TOTP_REQUIRED_GROUP, OVH::Bastion::MFA_TOTP_BYPASS_GROUP);
}
elsif ($key eq 'egress-strict-host-key-checking') {
osh_info "Changing the egress StrictHostKeyChecking option for this account...";
if (not grep { $value eq $_ } qw{ yes no ask default bypass }) {
osh_warn "Invalid parameter '$value', skipping";
$result{$jsonkey} = R('ERR_INVALID_PARAMETER');
}
else {
my $hostsFile; # undef, aka remove UserKnownHostsFile option
if ($value eq 'bypass') {
# special case: for 'bypass', we set Strict to no and UserKnownHostsFile to /dev/null
$value = 'no';
$hostsFile = '/dev/null';
}
elsif ($value eq 'default') {
# special case: for 'default', we actually remove the StrictHostKeyChecking option
undef $value;
}
$fnret = OVH::Bastion::account_ssh_config_set(account => $account, key => "StrictHostKeyChecking", value => $value);
$result{$jsonkey} = $fnret;
if ($fnret) {
$fnret = OVH::Bastion::account_ssh_config_set(account => $account, key => "UserKnownHostsFile", value => $hostsFile);
$result{$jsonkey} = $fnret;
}
if ($fnret) {
osh_info "... modification done";
}
else {
osh_warn "... error while setting StrictHostKeyChecking policy: " . $fnret->msg;
}
}
}
elsif ($key eq 'personal-egress-mfa-required') {
osh_info "Changing the MFA policy for egress connections using the personal access (and keys) of the account...";
if (not grep { $value eq $_ } qw{ password totp any none }) {
osh_warn "Invalid parameter '$value', skipping";
$result{$jsonkey} = R('ERR_INVALID_PARAMETER');
}
else {
$fnret = OVH::Bastion::account_config(account => $account, key => "personal_egress_mfa_required", value => $value);
$result{$jsonkey} = $fnret;
if ($fnret) {
osh_info "... modification done";
}
else {
osh_warn "... error while setting MFA policy: " . $fnret->msg;
}
}
}
}
HEXIT('OK', value => \%result);

View file

@ -0,0 +1,141 @@
#! /usr/bin/perl -T
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
# NEEDGROUP osh-accountGrantCommand
# NEEDGROUP osh-accountRevokeCommand
# SUDOERS # grant access to a command
# SUDOERS %osh-accountGrantCommand ALL=(root) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountModifyCommand --action grant *
# SUDOERS # revoke access to a command
# SUDOERS %osh-accountRevokeCommand ALL=(root) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountModifyCommand --action revoke *
# FILEMODE 0755
# FILEOWN root root
#>HEADER
use common::sense;
use Getopt::Long;
use File::Basename;
use lib dirname(__FILE__) . '/../../lib/perl';
use OVH::Bastion;
local $| = 1;
#
# Globals
#
$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/pkg/bin';
my ($self) = $ENV{'SUDO_USER'} =~ m{^([a-zA-Z0-9._-]+)$};
if (not defined $self) {
if ($< == 0) {
$self = 'root';
}
else {
HEXIT('ERR_SUDO_NEEDED', msg => 'This command must be run under sudo');
}
}
# Fetch command options
Getopt::Long::Configure("no_auto_abbrev");
my $fnret;
my ($result, @optwarns);
my ($action, $account, $command);
eval {
local $SIG{__WARN__} = sub { push @optwarns, shift };
$result = GetOptions(
"action=s" => sub { $action //= $_[1] },
"account=s" => sub { $account //= $_[1] },
"command=s" => sub { $command //= $_[1] },
);
};
if ($@) { die $@ }
if (!$result) {
local $" = ", ";
HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns");
}
if (not $account or not $command or not $action) {
HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'account', 'command' or 'action'");
}
#<HEADER
#>PARAMS:ACCOUNT
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account, localOnly => 1);
$fnret or HEXIT($fnret);
# get returned untainted value
$account = $fnret->value->{'account'};
#<PARAMS:ACCOUNT
#>PARAMS:ACTION
if ($action ne 'grant' && $action ne 'revoke') {
HEXIT('ERR_INVALID_PARAMETER', msg => "Parameter 'action' must be 'grant' or 'revoke'");
}
#<PARAMS:ACTION
#>PARAMS:COMMAND
if ($command =~ m{^([a-z0-9]+)$}i) {
$command = $1; # untaint
}
else {
HEXIT('ERR_INVALID_PARAMETER', msg => "Specified command is invalid ($command)");
}
#<PARAMS:COMMAND
#>RIGHTSCHECK
if ($self eq 'root') {
osh_debug "Real root, skipping checks of permissions";
}
elsif ($action eq 'grant') {
$fnret = OVH::Bastion::is_user_in_group(user => $self, group => "osh-accountGrantCommand");
if (!$fnret) {
HEXIT('ERR_SECURITY_VIOLATION', msg => "You're not allowed to run this, dear $self");
}
}
elsif ($action eq 'revoke') {
$fnret = OVH::Bastion::is_user_in_group(user => $self, group => "osh-accountRevokeCommand");
if (!$fnret) {
HEXIT('ERR_SECURITY_VIOLATION', msg => "You're not allowed to run this, dear $self");
}
}
#<RIGHTSCHECK
#>CODE
$fnret = OVH::Bastion::get_plugin_list(restrictedOnly => 1);
$fnret or HEXIT($fnret);
my @plugins = sort keys %{$fnret->value};
push @plugins, 'auditor';
if (!grep { $command eq $_ } @plugins) {
HEXIT('ERR_INVALID_PARAMETER', msg => "Specified command ($command) is not in the restricted plugins list");
}
if (grep { $command eq $_ } qw{ admin superowner }) {
HEXIT('ERR_SECURITY_VIOLATION', msg => "Specified command ($command) can't be granted this way for security reasons");
}
if (grep { $command eq $_ } qw{ accountGrantCommand accountRevokeCommand } && !OVH::Bastion::is_admin(sudo => 1)) {
HEXIT('ERR_SECURITY_VIOLATION', msg => "Specified command ($command) can only be granted by bastion admins for security reasons");
}
my $msg;
$fnret = OVH::Bastion::is_user_in_group(user => $account, group => "osh-$command");
if ($action eq 'grant') {
HEXIT('OK_NO_CHANGE', msg => "Account $account already has the right to use the $command plugin, no change required") if $fnret;
$fnret = OVH::Bastion::sys_addmembertogroup(user => $account, group => "osh-$command", noisy_stderr => 1);
$fnret or HEXIT($fnret);
$msg = "Successfully granted use of restricted command $command to $account";
}
elsif ($action eq 'revoke') {
HEXIT('OK_NO_CHANGE', msg => "Account $account did not have the right to use the $command plugin, no change required") if !$fnret;
$fnret = OVH::Bastion::sys_delmemberfromgroup(user => $account, group => "osh-$command", noisy_stderr => 1);
$fnret or HEXIT($fnret);
$msg = "Successfully revoked use of restricted command $command from $account";
}
HEXIT('OK', msg => $msg);

View file

@ -0,0 +1,106 @@
#! /usr/bin/perl -T
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
# NEEDGROUP osh-selfAddPersonalAccess
# SUDOERS %osh-selfAddPersonalAccess ALL=(allowkeeper) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountModifyPersonalAccess --target self --action add *
# FILEMODE 0750
# FILEOWN root allowkeeper
#
# NEEDGROUP osh-accountAddPersonalAccess
# SUDOERS %osh-accountAddPersonalAccess ALL=(allowkeeper) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountModifyPersonalAccess --target any --action add *
# FILEMODE 0750
# FILEOWN root allowkeeper
#
# NEEDGROUP osh-selfDelPersonalAccess
# SUDOERS %osh-selfDelPersonalAccess ALL=(allowkeeper) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountModifyPersonalAccess --target self --action del *
# FILEMODE 0750
# FILEOWN root allowkeeper
#
# NEEDGROUP osh-accountDelPersonalAccess
# SUDOERS %osh-accountDelPersonalAccess ALL=(allowkeeper) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountModifyPersonalAccess --target any --action del *
# FILEMODE 0750
# FILEOWN root allowkeeper
#>HEADER
use common::sense;
use Getopt::Long;
use File::Basename;
use lib dirname(__FILE__) . '/../../lib/perl';
use OVH::Bastion;
local $| = 1;
#
# Globals
#
$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/pkg/bin';
my ($self) = $ENV{'SUDO_USER'} =~ m{^([a-zA-Z0-9._-]+)$};
if (not defined $self) {
if ($< == 0) {
$self = 'root';
}
else {
HEXIT('ERR_SUDO_NEEDED', msg => 'This command must be run under sudo');
}
}
# Fetch command options
my $fnret;
my ($result, @optwarns);
my ($account, $ip, $user, $port, $action, $ttl, $forceKey, $target, $comment);
eval {
local $SIG{__WARN__} = sub { push @optwarns, shift };
$result = GetOptions(
"account=s" => sub { $account //= $_[1] },
"ip=s" => sub { $ip //= $_[1] },
"user=s" => sub { $user //= $_[1] },
"port=i" => sub { $port //= $_[1] },
"action=s" => sub { $action //= $_[1] },
"ttl=i" => sub { $ttl //= $_[1] },
"force-key=s" => sub { $forceKey //= $_[1] },
"target=s" => sub { $target //= $_[1] },
"comment=s" => sub { $comment //= $_[1] },
);
};
if ($@) { die $@ }
if (!$result) {
local $" = ", ";
HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns");
}
if (not $action or not $ip or not $account or not $target) {
HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'action' or 'ip' or 'account' or 'target'");
}
#<HEADER
not defined $account and $account = $self;
#>RIGHTSCHECK
if ($target eq 'self' && $self ne $account) {
HEXIT('ERR_SECURITY_VIOLATION', msg => "Attempted to modify another account while you're only allowed to do it on yourself");
}
#<RIGHTSCHECK
#>PARAMS:ACTION
if (not grep { $action eq $_ } qw{ add del }) {
return R('ERR_INVALID_PARAMETER', msg => "expected 'add' or 'del' as an action");
}
#<PARAMS:ACTION
#>CODE
# access_modify validates all its parameters, don't do it ourselves here for clarity
$fnret = OVH::Bastion::access_modify(
way => 'personal',
account => $account,
action => $action,
user => $user,
ip => $ip,
port => $port,
ttl => $ttl,
forceKey => $forceKey,
comment => $comment,
);
HEXIT($fnret);

187
bin/helper/osh-accountPIV Executable file
View file

@ -0,0 +1,187 @@
#! /usr/bin/perl -T
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
# NEEDGROUP osh-accountPIV
# SUDOERS # modify PIV policy of an account
# SUDOERS %osh-accountPIV ALL=(allowkeeper) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountPIV --step 1 --account *
# SUDOERS %osh-accountPIV ALL=(%bastion-users) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountPIV --step 2 --account *
# FILEMODE 0755
# FILEOWN root root
#>HEADER
use common::sense;
use Getopt::Long;
use File::Basename;
use lib dirname(__FILE__) . '/../../lib/perl';
use OVH::Bastion;
use OVH::Result;
local $| = 1;
#
# Globals
#
$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/pkg/bin';
my ($self) = $ENV{'SUDO_USER'} =~ m{^([a-zA-Z0-9._-]+)$};
if (not defined $self) {
if ($< == 0) {
$self = 'root';
}
else {
HEXIT('ERR_SUDO_NEEDED', msg => 'This command must be run under sudo');
}
}
# Fetch command options
Getopt::Long::Configure("no_auto_abbrev");
my $fnret;
my ($result, @optwarns);
my ($account, $policy, $ttl, $step);
eval {
local $SIG{__WARN__} = sub { push @optwarns, shift };
$result = GetOptions(
"account=s" => sub { $account //= $_[1] },
"policy=s" => sub { $policy //= $_[1] },
"step=i" => sub { $step //= $_[1] },
"ttl=i" => sub { $ttl //= $_[1] },
);
};
if ($@) { die $@ }
if (!$result) {
local $" = ", ";
HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns");
}
if (!$account || !$policy || !$step) {
HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'account' or 'modify' or 'step'");
}
#<HEADER
#>PARAMS:ACCOUNT
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account, localOnly => 1);
$fnret or HEXIT($fnret);
# get returned untainted value
$account = $fnret->value->{'account'};
#<PARAMS:ACCOUNT
#>PARAMS:POLICY
if (not grep { $policy eq $_ } qw{ none enforce grace }) {
HEXIT('ERR_INVALID_PARAMETER', "Expected either 'none,' enforce' of 'grace' as a parameter to --policy");
}
#<PARAMS:POLICY
#>PARAMS:TTL
if ($policy eq 'grace' && !defined $ttl) {
HEXIT('ERR_MISSING_PARAMETER', "The use of 'grace' requires to specify the --ttl parameter as well");
}
#<PARAMS:TTL
#>PARAMS:STEP
if ($step ne '1' && $step ne '2') {
HEXIT('ERR_INVALID_PARAMETER', "Only 1 or 2 are allowed for --step");
}
#<PARAMS:STEP
#>RIGHTSCHECK
if ($self eq 'root') {
osh_debug "Real root, skipping checks of permissions";
}
$fnret = OVH::Bastion::is_user_in_group(user => $self, group => "osh-accountPIV");
if (!$fnret) {
HEXIT('ERR_SECURITY_VIOLATION', msg => "You're not allowed to run this, dear $self");
}
if (OVH::Bastion::is_admin(account => $account, sudo => 1) && !OVH::Bastion::is_admin(account => $self, sudo => 1)) {
HEXIT('ERR_SECURITY_VIOLATION', msg => "You can't modify the account of an admin without being admin yourself");
}
#<RIGHTSCHECK
#>CODE
my $currentPolicy = OVH::Bastion::account_config(account => $account, key => OVH::Bastion::OPT_ACCOUNT_INGRESS_PIV_POLICY, public => 1);
if ($step == 1) {
# we're run under allowkeeper user, set config params where applicable
if ($policy eq 'enforce') {
if ($currentPolicy && $currentPolicy->value eq 'yes') {
HEXIT('OK_NO_CHANGE', msg => "PIV policy was already set to 'enforce' for this account, no change needed");
}
$fnret = OVH::Bastion::account_config(account => $account, key => OVH::Bastion::OPT_ACCOUNT_INGRESS_PIV_POLICY, public => 1, value => 'yes');
$fnret or HEXIT($fnret);
$fnret = OVH::Bastion::account_config(account => $account, key => OVH::Bastion::OPT_ACCOUNT_INGRESS_PIV_GRACE, public => 1, delete => 1);
# ignore error because maybe grace wasn't set
OVH::Bastion::syslogFormatted(
severity => 'info',
type => 'account',
fields => [['action', 'ingress-piv-policy'], ['account', $account], ['policy', 'enforce'],]
);
HEXIT('OK', msg => "PIV policy set to 'enforce' for this account");
}
elsif ($policy eq 'none') {
if (!$currentPolicy || $currentPolicy->value ne 'yes') {
HEXIT('OK_NO_CHANGE', msg => "PIV policy was already set to 'none' for this account, no change needed");
}
$fnret = OVH::Bastion::account_config(account => $account, key => OVH::Bastion::OPT_ACCOUNT_INGRESS_PIV_POLICY, public => 1, delete => 1);
$fnret or HEXIT($fnret);
$fnret = OVH::Bastion::account_config(account => $account, key => OVH::Bastion::OPT_ACCOUNT_INGRESS_PIV_GRACE, public => 1, delete => 1);
# ignore error because maybe grace wasn't set
OVH::Bastion::syslogFormatted(
severity => 'info',
type => 'account',
fields => [['action', 'ingress-piv-policy'], ['account', $account], ['policy', 'none'],]
);
HEXIT('OK', msg => "PIV policy set to 'none' for this account");
}
elsif ($policy eq 'grace') {
if (!$currentPolicy || $currentPolicy->value ne 'yes') {
HEXIT('OK_NO_CHANGE', msg => "PIV policy is not set to 'enforce' for this account, so no need for a grace period");
}
$fnret = OVH::Bastion::account_config(account => $account, key => OVH::Bastion::OPT_ACCOUNT_INGRESS_PIV_GRACE, public => 1, value => (time() + $ttl));
$fnret or HEXIT($fnret);
my $human = OVH::Bastion::duration2human(seconds => $ttl)->value;
OVH::Bastion::syslogFormatted(
severity => 'info',
type => 'account',
fields => [['action', 'ingress-piv-policy'], ['account', $account], ['policy', 'grace'], ['duration', $human->{'duration'}],]
);
HEXIT('OK', msg => "PIV policy is now in grace mode for " . $human->{'duration'} . " (until " . $human->{'date'} . ")");
}
# unreachable
HEXIT('ERR_INTERNAL', msg => "Unknown policy specified (step 1)");
}
elsif ($step == 2) {
# now we're running under the own account's user, modify the authkeys file accordingly
my $pivAction;
if ($policy eq 'enforce') {
$pivAction = 'enable';
}
elsif ($policy eq 'none' || $policy eq 'grace') {
$pivAction = 'disable';
}
else {
# unreachable
HEXIT('ERR_INTERNAL', msg => "Unknown policy specified (step 2)");
}
$fnret = OVH::Bastion::ssh_ingress_keys_piv_apply(action => $pivAction, account => $account);
$fnret or HEXIT($fnret);
if ($pivAction eq 'enable') {
HEXIT('OK', msg => "All non-PIV account's ingress keys have been disabled");
}
else {
HEXIT('OK', msg => "Non-PIV account's ingress keys, if any, have been restored");
}
}
# unreachable
HEXIT('ERR_INTERNAL', msg => "Unknown step specified");

95
bin/helper/osh-accountUnexpire Executable file
View file

@ -0,0 +1,95 @@
#! /usr/bin/perl -T
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
# NEEDGROUP osh-accountUnexpire
# SUDOERS %osh-accountUnexpire ALL=(%bastion-users) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-accountUnexpire *
# FILEMODE 0755
# FILEOWN root root
#>HEADER
use common::sense;
use Getopt::Long;
use File::Basename;
use lib dirname(__FILE__) . '/../../lib/perl';
use OVH::Bastion;
local $| = 1;
#
# Globals
#
$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/pkg/bin';
my ($self) = $ENV{'SUDO_USER'} =~ m{^([a-zA-Z0-9._-]+)$};
if (not defined $self) {
if ($< == 0) {
$self = 'root';
}
else {
HEXIT('ERR_SUDO_NEEDED', msg => 'This command must be run under sudo');
}
}
# Fetch command options
my $fnret;
my ($result, @optwarns);
my ($account);
eval {
local $SIG{__WARN__} = sub { push @optwarns, shift };
$result = GetOptions("account=s" => sub { $account //= $_[1] });
};
if ($@) { die $@ }
if (!$result) {
local $" = ", ";
HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns");
}
if (!$account) {
HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'account'");
}
#<HEADER
#>RIGHTSCHECK
if ($self eq 'root') {
osh_debug "Real root, skipping checks of permissions";
}
else {
# need to perform another security check
$fnret = OVH::Bastion::is_user_in_group(user => $self, group => "osh-accountUnexpire");
if (!$fnret) {
HEXIT('ERR_SECURITY_VIOLATION', msg => "You're not allowed to run this, dear $self");
}
}
# for this special helper, $account must be equal to $ENV{'USER'}
if (OVH::Bastion::get_user_from_env()->value ne $account) {
HEXIT('ERR_SECURITY_VIOLATION', msg => "You're not allowed to run this on $account, dear $self");
}
#<RIGHTSCHECK
#>PARAMS:ACCOUNT
osh_debug("Checking account");
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account);
$fnret or HEXIT($fnret);
$account = $fnret->value->{'account'}; # untainted
#<PARAMS:ACCOUNT
my $accounthome = $fnret->value->{'dir'};
if (!-d $accounthome) {
HEXIT('ERR_INVALID_HOME', msg => "Invalid HOME directory for this account");
}
$fnret = OVH::Bastion::is_account_nonexpired(sysaccount => $account);
$fnret->is_err and HEXIT($fnret); # couldn't read file or other error
$fnret->is_ok and HEXIT($fnret); # wasn't expired
# is_ko: is expired
my $days = $fnret->value->{'days'};
my $filepath = $fnret->value->{'filepath'};
$fnret = OVH::Bastion::touch_file($filepath);
$fnret or HEXIT($fnret);
HEXIT('OK', value => {account => $account, days => $days});

113
bin/helper/osh-adminMaintenance Executable file
View file

@ -0,0 +1,113 @@
#! /usr/bin/perl -T
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
# NEEDGROUP osh-admin
# SUDOERS %osh-admin ALL=(allowkeeper) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-adminMaintenance *
# FILEMODE 0750
# FILEOWN root allowkeeper
#>HEADER
use common::sense;
use Getopt::Long;
use File::Basename;
use lib dirname(__FILE__) . '/../../lib/perl';
use OVH::Bastion;
local $| = 1;
#
# Globals
#
$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/pkg/bin';
my ($self) = $ENV{'SUDO_USER'} =~ m{^([a-zA-Z0-9._-]+)$};
if (not defined $self) {
if ($< == 0) {
$self = 'root';
}
else {
HEXIT('ERR_SUDO_NEEDED', msg => 'This command must be run under sudo');
}
}
# Fetch command options
my $fnret;
my ($result, @optwarns);
my ($action, $message);
eval {
local $SIG{__WARN__} = sub { push @optwarns, shift };
$result = GetOptions(
"action=s" => sub { $action //= $_[1] },
"message=s" => sub { $message //= $_[1] },
);
};
if ($@) { die $@ }
if (!$result) {
local $" = ", ";
HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns");
}
if (not $action) {
HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'action'");
}
if (not grep { $action eq $_ } qw{ set unset }) {
HEXIT('ERR_INVALID_PARAMETER', msg => "Expected action 'set' or 'unset'");
}
#<HEADER
#>RIGHTSCHECK
if ($self eq 'root') {
osh_debug "Real root, skipping checks of permissions";
}
else {
# need to perform another security check
$fnret = OVH::Bastion::is_user_in_group(user => $self, group => "osh-admin");
if (!$fnret) {
HEXIT('ERR_SECURITY_VIOLATION', msg => "You're not allowed to run this, dear $self");
}
}
#<RIGHTSCHECK
#>CODE
my $retmsg;
if ($action eq 'set') {
if (-e '/home/allowkeeper/maintenance') {
HEXIT('OK_NO_CHANGE', msg => "Nothing to do, maintenance mode was already set");
}
$fnret = OVH::Bastion::touch_file('/home/allowkeeper/maintenance', 0644); ## no critic (ProhibitLeadingZeros)
if (!$fnret) {
HEXIT('KO', msg => "Couldn't set the bastion to maintenance mode (" . $fnret->msg . ")");
}
$message = "(no reason given)" if not $message;
$message .= " [set by $self at " . localtime(time()) . "]";
if (open(my $fh, '>', '/home/allowkeeper/maintenance')) {
print $fh $message;
}
else {
osh_warn "Couldn't write the maintenance message ($!), but we're still setting the maintenance mode, users just won't see your maintenance message.";
}
$retmsg = "Maintenance mode is now enabled, new connections are disallowed (except for admins).\nGiven reason: $message";
}
elsif ($action eq 'unset') {
if (-e '/home/allowkeeper/maintenance') {
if (!unlink('/home/allowkeeper/maintenance')) {
HEXIT('KO', msg => "Couldn't unset the bastion maintenance mode ($!)");
}
}
else {
HEXIT('OK_NO_CHANGE', msg => "Nothing to do, maintenance mode was not set previously");
}
$retmsg = "Maintenance mode is now disabled, new connections are allowed.";
}
OVH::Bastion::syslogFormatted(
severity => 'info',
type => 'maintenance',
fields => [['action', $action], ['message', $message],]
);
# done at last!
HEXIT('OK', value => {action => $action, message => $message}, msg => $retmsg);

114
bin/helper/osh-groupAddServer Executable file
View file

@ -0,0 +1,114 @@
#! /usr/bin/perl -T
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
# KEYSUDOERS # as an aclkeeper, we can add/del a server from the group server list in /home/%GROUP%/allowed.ip
# KEYSUDOERS SUPEROWNERS, %%GROUP%-aclkeeper ALL=(%GROUP%) NOPASSWD: /usr/bin/env perl -T %BASEPATH%/bin/helper/osh-groupAddServer --group %GROUP% *
# FILEMODE 0755
# FILEOWN root root
#>HEADER
use common::sense;
use Getopt::Long;
use Net::IP;
use File::Basename;
use lib dirname(__FILE__) . '/../../lib/perl';
use OVH::Result;
use OVH::Bastion;
local $| = 1;
#
# Globals
#
$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/pkg/bin';
my ($self) = $ENV{'SUDO_USER'} =~ m{^([a-zA-Z0-9._-]+)$};
if (not defined $self) {
if ($< == 0) {
$self = 'root';
}
else {
HEXIT('ERR_SUDO_NEEDED', msg => 'This command must be run under sudo');
}
}
# Fetch command options
my $fnret;
my ($result, @optwarns);
my ($group, $user, $ip, $port, $action, $force, $ttl, $comment);
eval {
local $SIG{__WARN__} = sub { push @optwarns, shift };
$result = GetOptions(
"group=s" => sub { $group //= $_[1] }, # ignore subsequent --group on cmdline (anti-sudoers-override)
"user=s" => sub { $user //= $_[1] },
"ip=s" => sub { $ip //= $_[1] },
"port=i" => sub { $port //= $_[1] },
"action=s" => sub { $action //= $_[1] },
"force" => sub { $force //= $_[1] },
"ttl=i" => sub { $ttl //= $_[1] },
"comment=s" => sub { $comment //= $_[1] },
);
};
if ($@) { die $@ }
if (!$result) {
local $" = ", ";
HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns");
}
if (not $ip or not $group or not $action) {
HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'ip' or 'group' or 'action'");
}
if (not grep { $action eq $_ } qw{ add del }) {
HEXIT('ERR_INVALID_PARAMETER', msg => "Argument action should be 'add' or 'del'");
}
#<HEADER
#>PARAMS:GROUP
osh_debug("Checking group $group");
$fnret = OVH::Bastion::is_valid_group_and_existing(group => $group, groupType => 'key');
$fnret or HEXIT($fnret);
# get returned untainted value
$group = $fnret->value->{'group'};
my $shortGroup = $fnret->value->{'shortGroup'};
osh_debug("got group $group/$shortGroup");
#<PARAMS:GROUP
#>RIGHTSCHECK
if ($self eq 'root') {
osh_debug "Real root, skipping checks of permissions";
}
else {
$fnret = OVH::Bastion::is_group_aclkeeper(account => $self, group => $shortGroup, sudo => 1, superowner => 1);
$fnret or HEXIT('ERR_NOT_ALLOWED', msg => "Sorry, you must be an aclkeeper of group $shortGroup");
}
#<RIGHTSCHECK
#>CODE
my $machine = $ip;
$port and $machine .= ":$port";
$user and $machine = $user . '@' . $machine;
# access_modify validates all its parameters, don't do it ourselves here for clarity
$fnret = OVH::Bastion::access_modify(
way => 'group',
action => $action,
group => $group,
ip => $ip,
user => $user,
port => $port,
ttl => $ttl,
comment => $comment,
);
if ($fnret->err eq 'OK') {
my $ttlmsg = $ttl ? ' (expires in ' . OVH::Bastion::duration2human(seconds => $ttl)->value->{'human'} . ')' : '';
HEXIT('OK', msg => $action eq 'add' ? "Entry $machine was added to group $shortGroup$ttlmsg" : "Entry $machine was removed from group $shortGroup$ttlmsg");
}
elsif ($fnret->err eq 'OK_NO_CHANGE') {
HEXIT('OK_NO_CHANGE',
msg => $action eq 'add' ? "Entry $machine was already added to group $shortGroup, nothing done" : "Entry $machine was not in group $shortGroup, nothing done");
}
HEXIT($fnret);

View file

@ -0,0 +1,147 @@
#! /usr/bin/perl -T
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
# KEYSUDOERS # as a gatekeeper, to be able to symlink in /home/allowkeeper/ACCOUNT the /home/%GROUP%/allowed.ip file
# KEYSUDOERS SUPEROWNERS, %%GROUP%-gatekeeper ALL=(allowkeeper) NOPASSWD: /usr/bin/env perl -T %BASEPATH%/bin/helper/osh-groupAddSymlinkToAccount --group %GROUP% *
# FILEMODE 0750
# FILEOWN root allowkeeper
#>HEADER
use common::sense;
use Getopt::Long;
use File::Basename;
use lib dirname(__FILE__) . '/../../lib/perl';
use OVH::Bastion;
local $| = 1;
#
# Globals
#
$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/pkg/bin';
my ($self) = $ENV{'SUDO_USER'} =~ m{^([a-zA-Z0-9._-]+)$};
if (not defined $self) {
if ($< == 0) {
$self = 'root';
}
else {
HEXIT('ERR_SUDO_NEEDED', msg => 'This command must be run under sudo');
}
}
# Fetch command options
my $fnret;
my ($result, @optwarns);
my ($account, $group, $action);
eval {
local $SIG{__WARN__} = sub { push @optwarns, shift };
$result = GetOptions(
"account=s" => sub { $account //= $_[1] },
"group=s" => sub { $group //= $_[1] }, # ignore subsequent --group on cmdline (anti-sudoers-override)
"action=s" => sub { $action //= $_[1] },
);
};
if ($@) { die $@ }
if (!$result) {
local $" = ", ";
HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns");
}
if (not $account or not $group or not $action) {
HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'account', 'group' or 'action'");
}
if (not grep { $action eq $_ } qw{ add del }) {
HEXIT('ERR_INVALID_PARAMETER', msg => "Argument action should be either 'add' or 'del'");
}
#<HEADER
#>PARAMS:ACCOUNT
osh_debug("Checking account");
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account);
$fnret or HEXIT($fnret);
# get returned untainted value
$account = $fnret->value->{'account'};
my $sysaccount = $fnret->value->{'sysaccount'};
my $remoteaccount = $fnret->value->{'remoteaccount'};
#<PARAMS:ACCOUNT
#>PARAMS:GROUP
# test if start by key, append if necessary
if ($group !~ /^key/) {
$group = "key$group";
}
osh_debug("Checking group");
$fnret = OVH::Bastion::is_valid_group_and_existing(group => $group, groupType => 'key');
$fnret or HEXIT($fnret);
# get returned untainted value
$group = $fnret->value->{'group'};
my $shortGroup = $fnret->value->{'shortGroup'};
#<PARAMS:GROUP
#>RIGHTSCHECK
if ($self eq 'root') {
osh_debug "Real root, skipping checks of permissions";
}
else {
$fnret = OVH::Bastion::is_group_gatekeeper(account => $self, group => $shortGroup, superowner => 1, sudo => 1);
if (!$fnret) {
warn_syslog("$0: account $self is not a $shortGroup gatekeeper, refused to continue");
HEXIT('ERR_NOT_ALLOWED', msg => "Sorry, you're not a gatekeeper of group $shortGroup");
}
}
#<RIGHTSCHECK
osh_debug("user -gatek or gatek");
#>CODE
my $msg;
my $prefix = $remoteaccount ? "allowed_$remoteaccount" : "allowed";
my $link = "/home/allowkeeper/$sysaccount/$prefix.ip.$shortGroup";
if ($action eq 'del') {
osh_debug("Going to remove symlink");
if (-l $link || -e _) {
if (unlink $link) {
$msg = "Successfully removed $link";
}
else {
warn_syslog("$0: error while trying to remove symlink $link ($!)");
HEXIT('ERR_UNLINK_FAILED', msg => "Error while trying to remove symlink");
}
}
else {
HEXIT('OK_NO_CHANGE', msg => "Symlink was not existing as $link, nothing to do");
}
}
elsif ($action eq 'add') {
my $source = "/home/$group/allowed.ip";
osh_debug("symlinking $source to $link");
if (not -e $source) {
HEXIT('ERR_SOURCE_NOT_FOUND', msg => "Cannot create symlink as $source doesn't exist");
}
elsif (-e $link) {
HEXIT('OK_NO_CHANGE', msg => "Symlink $link is already there, nothing to do");
}
else {
if (symlink($source, $link)) {
$msg = "Account $account now has full access to $shortGroup servers";
}
else {
warn_syslog("$0: error while creating symlink $source to $link ($!)");
HEXIT('ERR_SYMLINK_FAILED', msg => "Error while creating symlink");
}
}
}
else {
warn_syslog("$0: unreachable code has been reached");
HEXIT('ERR_INTERNAL'); # unreachable
}
HEXIT("OK", msg => $msg);

287
bin/helper/osh-groupCreate Executable file
View file

@ -0,0 +1,287 @@
#! /usr/bin/perl -T
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
# NEEDGROUP osh-groupCreate
# SUDOERS %osh-groupCreate ALL=(root) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-groupCreate *
# FILEMODE 0700
# FILEOWN root root
#>HEADER
use common::sense;
use Getopt::Long;
use Term::ReadKey;
use File::Basename;
use lib dirname(__FILE__) . '/../../lib/perl';
use OVH::Bastion;
use OVH::Bastion::Plugin::groupSetRole;
local $| = 1;
#
# Globals
#
$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/pkg/bin';
my ($self) = $ENV{'SUDO_USER'} =~ m{^([a-zA-Z0-9._-]+)$};
if (not defined $self) {
if ($< == 0) {
$self = 'root';
}
else {
HEXIT('ERR_SUDO_NEEDED', msg => 'This command must be run under sudo');
}
}
# Fetch command options
my $fnret;
my ($result, @optwarns);
my ($group, $owner, $algo, $size, $encrypted, $no_key, $comment);
eval {
local $SIG{__WARN__} = sub { push @optwarns, shift };
$result = GetOptions(
"group=s" => sub { $group //= $_[1] },
"owner=s" => sub { $owner //= $_[1] },
"algo=s" => sub { $algo //= $_[1] },
"size=i" => sub { $size //= $_[1] },
"encrypted" => sub { $encrypted //= $_[1] },
"no-key" => sub { $no_key //= $_[1] },
"comment=s" => sub { $comment //= $_[1] },
);
};
if ($@) { die $@ }
if (!$result) {
local $" = ", ";
HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns");
}
if (!$group || !$owner) {
HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'group' or 'owner'");
}
if ($no_key && ($algo || $size || $encrypted)) {
EXIT('ERR_INVALID_PARAMETER', msg => "Can't specify 'no-key' along with 'algo', 'size' or 'encrypted'");
}
if (!$no_key && (!$algo || !$size)) {
HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'algo' or 'size'");
}
if ($comment) {
if ($comment =~ /^([a-zA-Z0-9=_,-]+)$/) {
$comment = $1; # untaint
}
else {
HEXIT('ERR_INVALID_PARAMETER', msg => "Specified comment contains invalid characters");
}
}
#<HEADER
#>RIGHTSCHECK
if ($self eq 'root') {
osh_debug "Real root, skipping checks of permissions";
}
else {
# need to perform another security check
$fnret = OVH::Bastion::is_user_in_group(user => $self, group => "osh-groupCreate");
if (!$fnret) {
HEXIT('ERR_SECURITY_VIOLATION', msg => "You're not allowed to run this, dear $self");
}
}
#<RIGHTSCHECK
#>PARAMS:ACCOUNT
osh_debug("Checking owner");
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $owner);
$fnret or HEXIT($fnret);
# get returned untainted value
$owner = $fnret->value->{'account'};
#<PARAMS:ACCOUNT
#>PARAMS:GROUP
osh_debug("checking group");
$fnret = OVH::Bastion::is_valid_group(group => $group, groupType => "key");
$fnret or HEXIT($fnret);
# get returned untainted value
$group = $fnret->value->{'group'};
my $shortGroup = $fnret->value->{'shortGroup'};
foreach my $test ($group, "$group-gatekeeper", "$group-owner") {
$fnret = OVH::Bastion::is_group_existing(group => $test);
$fnret->is_err and HEXIT($fnret);
my (undef, $displayGroup) = $test =~ m/^(key)?(.+)/;
$fnret->is_ok and HEXIT('KO_ALREADY_EXISTING', msg => "The group $displayGroup already exists");
}
$fnret = OVH::Bastion::is_account_existing(account => $group);
$fnret->is_err and HEXIT($fnret);
$fnret->is_ok and HEXIT('KO_ALREADY_EXISTING', msg => "The account $group already exists");
#<PARAMS:GROUP
#>PARAMS:ALGO/SIZE
if (!$no_key) {
$algo = lc($algo);
$fnret = OVH::Bastion::is_allowed_algo_and_size(algo => $algo, size => $size, way => 'egress');
$fnret or HEXIT($fnret);
# if we're still here, it's valid, untaint those
($algo) = $algo =~ m/(.+)/;
($size) = $size =~ m/(.+)/;
}
#<PARAMS:ALGO/SIZE
#>CODE
my $passphrase = ''; # empty by default
if ($encrypted) {
print STDERR "Please enter a passphrase for the new group key (not echoed): ";
ReadMode('noecho');
chomp(my $pass1 = <STDIN>);
if (length($pass1) < 5) {
ReadMode('restore');
HEXIT('ERR_PASSPHRASE_TOO_SMALL', msg => "Passphrase should have at least 5 chars");
}
print STDERR "\nPlease enter it again: ";
chomp(my $pass2 = <STDIN>);
print STDERR "\n";
ReadMode('restore');
if ($pass1 ne $pass2) {
HEXIT('ERR_PASSPHRASE_MISMATCH', msg => "Passphrases don't match, please try again");
}
($passphrase) = $pass1 =~ /(.+)/; # untaint
}
# First create group
osh_info("Creating groups");
foreach my $tocreate ($group, "$group-aclkeeper", "$group-gatekeeper", "$group-owner") {
$fnret = OVH::Bastion::sys_groupadd(group => $tocreate, noisy_stderr => 1);
$fnret->err eq 'OK'
or HEXIT('ERR_GROUPADD_FAILED', msg => "Error while running groupadd command for $tocreate (" . $fnret->msg . ")");
}
osh_debug("Creating directory");
mkdir "/home/keykeeper/$group";
chmod 0755, "/home/keykeeper/$group";
osh_info("Creating user corresponding to group $shortGroup");
# if a comment has been set, we'll store it as the user's GECOS corresponding to the group name
# user is member of the group, cannot login and have no password
$fnret = OVH::Bastion::sys_useradd(user => $group, gid => $group, shell => undef, comment => $comment, noisy_stderr => 1);
$fnret->err eq 'OK'
or HEXIT('ERR_USERADD_FAILED', msg => "Error while adding corresponding user of group $shortGroup (" . $fnret->msg . ")");
# Building /home/$group
OVH::Bastion::touch_file("/home/$group/allowed.ip");
osh_debug("Adding allowkeeper to group $group");
$fnret = OVH::Bastion::add_user_to_group(group => $group, user => 'allowkeeper', groupType => 'key');
$fnret or HEXIT($fnret);
osh_info("Adding $owner to owner, gatekeeper, aclkeeper and main groups of $shortGroup");
# temporarily set ourselves owner manually so that we can add the wanted owner properly
# as owner/gatekeeper/member then revoke our own right
$fnret = OVH::Bastion::sys_addmembertogroup(group => "$group-owner", user => $self, noisy_stderr => 1);
$fnret or HEXIT($fnret);
# special case: if we're setting ourselves as owner, we must not remove
# our own rights after granting
my @todoList = (
$owner eq $self
? (
{action => 'add', type => 'owner', account => $owner},
{action => 'add', type => 'aclkeeper', account => $owner},
{action => 'add', type => 'gatekeeper', account => $owner},
{action => 'add', type => 'member', account => $owner},
)
: (
{action => 'add', type => 'owner', account => $owner},
{action => 'add', type => 'aclkeeper', account => $owner},
{action => 'add', type => 'gatekeeper', account => $owner},
{action => 'add', type => 'gatekeeper', account => $self},
{action => 'add', type => 'member', account => $owner},
{action => 'del', type => 'gatekeeper', account => $self},
{action => 'del', type => 'owner', account => $self},
)
);
foreach my $todo (@todoList) {
$fnret = OVH::Bastion::Plugin::groupSetRole::act(
self => $self,
account => $todo->{'account'},
group => $shortGroup,
action => $todo->{'action'},
type => $todo->{'type'},
sudo => 1,
silentoverride => 1
);
$fnret or HEXIT($fnret);
}
my $keykeeper_uid = (getpwnam('keykeeper'))[2];
my $group_gid = (getgrnam($group))[2];
chown $keykeeper_uid, $group_gid, "/home/keykeeper/$group";
if (!$no_key) {
osh_info("Generating main group key, this might take a few seconds...");
$fnret = OVH::Bastion::generate_ssh_key(
prefix => $shortGroup,
folder => "/home/keykeeper/$group",
size => $size,
algo => $algo,
passphrase => $passphrase,
uid => $keykeeper_uid,
gid => $group_gid,
group_readable => 1
);
$fnret or HEXIT($fnret);
}
osh_info("Adjusting permissions...");
my $bigX = (OVH::Bastion::is_linux() ? 'X' : 'x');
foreach my $command (
['chown', '-R', "$group:$group", "/home/$group"],
['chgrp', "$group-aclkeeper", "/home/$group/allowed.ip"],
['chmod', '-R', "o-rwx,g=r$bigX,u=rw$bigX", "/home/$group"],
['chmod', '0664', "/home/$group/allowed.ip"],
)
{
$fnret = OVH::Bastion::execute(cmd => $command, noisy_stderr => 1);
$fnret->err eq 'OK'
or HEXIT('ERR_CHMOD_FAILED', msg => "Error while running chmod to adjust permissions (" . $fnret->msg . ")");
}
chmod 0751, "/home/$group" if !OVH::Bastion::has_acls();
foreach my $gr ("$group-owner", "$group-gatekeeper", "$group-aclkeeper", "osh-whoHasAccessTo", "osh-auditor") {
OVH::Bastion::sys_setfacl(target => "/home/$group", perms => "g:$gr:x")
or HEXIT('ERR_SETFACL_FAILED', msg => "Error setting ACLs on group homedir");
}
# allowed to sudo for the group
osh_info("Configuring sudoers for this group");
my $sudoers_dir = OVH::Bastion::sys_getsudoersfolder();
if (-e "$sudoers_dir/osh-group-$group") {
osh_debug "sudoers already in place, but overwriting it";
}
$fnret = OVH::Bastion::execute(cmd => [$OVH::Bastion::BASEPATH . '/bin/sudogen/generate-sudoers.sh', 'group', $group], must_succeed => 1, noisy_stdout => 1);
$fnret or HEXIT('ERR_CANNOT_CREATE_SUDOERS', msg => "An error occurred while creating sudoers for this group");
OVH::Bastion::syslogFormatted(
severity => 'info',
type => 'group',
fields => [
['action', 'create'],
['group', $shortGroup],
['owner', $owner],
['egress_ssh_key_algorithm', $algo],
['egress_ssh_key_size', $size],
['egress_ssh_key_encrypted', ($encrypted ? 'true' : 'false')],
]
);
# done at last!
HEXIT('OK', value => {group => $shortGroup, owner => $owner});

196
bin/helper/osh-groupDelete Executable file
View file

@ -0,0 +1,196 @@
#! /usr/bin/perl -T
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
# NEEDGROUP osh-groupDelete
# SUDOERS %osh-groupDelete ALL=(root) NOPASSWD:/usr/bin/env perl -T /opt/bastion/bin/helper/osh-groupDelete *
# FILEMODE 0700
# FILEOWN root root
#>HEADER
use common::sense;
use Getopt::Long;
use File::Copy qw(move);
use File::Basename;
use lib dirname(__FILE__) . '/../../lib/perl';
use OVH::Bastion;
local $| = 1;
#
# Globals
#
$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/pkg/bin';
my ($self) = $ENV{'SUDO_USER'} =~ m{^([a-zA-Z0-9._-]+)$};
if (not defined $self) {
if ($< == 0) {
$self = 'root';
}
else {
HEXIT('ERR_SUDO_NEEDED', msg => 'This command must be run under sudo');
}
}
# Fetch command options
my $fnret;
my ($result, @optwarns);
my ($group);
eval {
local $SIG{__WARN__} = sub { push @optwarns, shift };
$result = GetOptions("group=s" => sub { $group //= $_[1] },);
};
if ($@) { die $@ }
if (!$result) {
local $" = ", ";
HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns");
}
if (!$group) {
HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'group'");
}
#<HEADER
#>RIGHTSCHECK
if ($self eq 'root') {
osh_debug "Real root, skipping checks of permissions";
}
else {
# need to perform another security check
$fnret = OVH::Bastion::is_user_in_group(user => $self, group => "osh-groupDelete");
if (!$fnret) {
HEXIT('ERR_SECURITY_VIOLATION', msg => "You're not allowed to run this, dear $self");
}
}
#<RIGHTSCHECK
#>PARAMS:GROUP
# test if start by key, append if necessary
osh_debug("Checking group");
$fnret = OVH::Bastion::is_valid_group_and_existing(group => $group, groupType => "key");
$fnret or HEXIT($fnret);
# get returned untainted value
$group = $fnret->value->{'group'};
my $shortGroup = $fnret->value->{'shortGroup'};
#<PARAMS:GROUP
#>CODE
# last security check
if (not -e "/home/$group/allowed.ip" or not -e "/home/keykeeper/$group") {
HEXIT('ERR_INVALID_GROUP', msg => "Sorry, but $shortGroup doesn't seem to be a legit bastion group");
}
if (not -d "/home/oldkeeper") {
mkdir "/home/oldkeeper";
}
chown 0, 0, "/home/oldkeeper";
chmod 0700, "/home/oldkeeper";
if (!-d "/home/oldkeeper/groups") {
mkdir "/home/oldkeeper/groups";
}
chown 0, 0, "/home/oldkeeper/groups";
chmod 0700, "/home/oldkeeper/groups";
my $suffix = 'at-' . time() . '.by-' . $self;
my $fulldir = "/home/oldkeeper/groups/$group.$suffix";
if (-e $fulldir) {
exitError("Errr... $fulldir exists?!");
}
mkdir $fulldir;
chown 0, 0, $fulldir;
chmod 0700, $fulldir;
move("/home/$group", "$fulldir/$group-home");
move("/home/keykeeper/$group", "$fulldir/$group-keykeeper");
# now tar.gz the directory, this is important because inside we'll keep the
# old GID of the group, and we don't want GID-orphaned files on our filesystem, it's
# not a problem to have those inside a tarfile however.
my @tarcmd = qw{ tar czf };
push @tarcmd, $fulldir . '.tar.gz';
push @tarcmd, '--acls' if OVH::Bastion::has_acls();
push @tarcmd, '--one-file-system', '-p', '--remove-files', $fulldir;
$fnret = OVH::Bastion::execute(cmd => \@tarcmd, must_succeed => 1);
if (!$fnret) {
osh_warn("Couldn't tar the backup homedir of this group (" . $fnret->msg . "), proceeding anyway.");
chmod 0000, $fulldir;
}
else {
chmod 0000, $fulldir . '.tar.gz';
unlink($fulldir);
}
# remove dead symlinks in users homes
my $dh;
if (opendir($dh, "/home/allowkeeper")) {
while (my $dir = readdir($dh)) {
$dir =~ /^\./ and next;
$dir !~ /^([a-zA-Z0-9._-]+)$/ and next;
$dir = "/home/allowkeeper/$1"; # and untaint
-d $dir or next;
foreach my $file ("$dir/allowed.ip.$shortGroup", "$dir/allowed.partial.$shortGroup") {
if (-e $file || -l $file) {
osh_info "Removing $file...";
unlink($file);
}
}
}
close($dh);
}
else {
osh_warn("Couldn't open /home/allowkeeper ?!");
}
# trying to remove main and gatekeeper and owner groups
foreach my $todelete ("$group-owner", "$group-aclkeeper", "$group-gatekeeper", $group) {
$fnret = OVH::Bastion::is_group_existing(group => $todelete);
if ($fnret) {
$todelete = $fnret->value->{'group'}; # untaint
my $members = $fnret->value->{'members'} || [];
if (@$members) {
osh_info "Found " . (scalar @$members) . " members, removing them from the group";
foreach my $member (@$members) {
osh_info "... removing $member from group $todelete";
$fnret = OVH::Bastion::sys_delmemberfromgroup(user => $member, group => $todelete, noisy_stderr => 1);
$fnret->err eq 'OK'
or HEXIT('ERR_DELUSER_FAILED', msg => "Error while attempting to remove member $member from group $todelete (" . $fnret->msg . ")");
}
}
if ($todelete eq $group) {
osh_info "Deleting main user of group $todelete...", $fnret = OVH::Bastion::sys_userdel(user => $todelete, noisy_stderr => 1);
$fnret->err eq 'OK'
or HEXIT('ERR_DELUSER_FAILED', msg => "Error while attempting to delete main user of group $todelete (" . $fnret->msg . ")");
}
# some OSes delete the main group of user if it has the same name
# and nobody else is a member of it, so check it still exists before
# trying to delete it
$fnret = OVH::Bastion::is_group_existing(group => $todelete);
if ($fnret) {
osh_info "Deleting group $todelete...";
$fnret = OVH::Bastion::sys_groupdel(group => $todelete, noisy_stderr => 1);
$fnret
or HEXIT('ERR_DELGROUP_FAILED', msg => "Error while attempting to delete group $todelete (" . $fnret->msg . ")");
}
}
else {
osh_info "Group $todelete not found, ignoring...";
}
}
# remove sudoers if it's there
unlink(OVH::Bastion::sys_getsudoersfolder() . "/osh-group-$group");
OVH::Bastion::syslogFormatted(
severity => 'info',
type => 'group',
fields => [['action', 'delete'], ['group', $shortGroup],]
);
HEXIT('OK');

View file

@ -0,0 +1,56 @@
#! /usr/bin/perl -T
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
# KEYSUDOERS # as an owner, we can generate an egress password for the group
# KEYSUDOERS SUPEROWNERS, %%GROUP%-owner ALL=(%GROUP%) NOPASSWD: /usr/bin/env perl -T %BASEPATH%/bin/helper/osh-groupGeneratePassword --group %GROUP% *
# FILEMODE 0755
# FILEOWN root root
#>HEADER
use common::sense;
use Getopt::Long;
use File::Basename;
use lib dirname(__FILE__) . '/../../lib/perl';
use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin::generatePassword;
local $| = 1;
#
# Globals
#
$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/pkg/bin';
my ($self) = $ENV{'SUDO_USER'} =~ m{^([a-zA-Z0-9._-]+)$};
if (not defined $self) {
if ($< == 0) {
$self = 'root';
}
else {
HEXIT('ERR_SUDO_NEEDED', msg => 'This command must be run under sudo');
}
}
# Fetch command options
my ($result, @optwarns);
my ($group, $size);
eval {
local $SIG{__WARN__} = sub { push @optwarns, shift };
$result = GetOptions(
"group=s" => sub { $group //= $_[1] }, # ignore subsequent --group on cmdline (anti-sudoers-override)
"size=i" => sub { $size //= $_[1] },
);
};
if ($@) { die $@ }
if (!$result) {
local $" = ", ";
HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns");
}
if (not $size or not $group) {
HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'size' or 'group'");
}
#<HEADER
HEXIT(OVH::Bastion::Plugin::generatePassword::act(self => $self, context => 'group', group => $group, size => $size, sudo => 1));

128
bin/helper/osh-groupModify Executable file
View file

@ -0,0 +1,128 @@
#! /usr/bin/perl -T
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
# KEYSUDOERS # as an owner, we can modify the group settings
# KEYSUDOERS SUPEROWNERS, %%GROUP%-owner ALL=(%GROUP%) NOPASSWD: /usr/bin/env perl -T %BASEPATH%/bin/helper/osh-groupModify --group %GROUP% *
# FILEMODE 0755
# FILEOWN root root
#>HEADER
use common::sense;
use Getopt::Long;
use File::Basename;
use lib dirname(__FILE__) . '/../../lib/perl';
use OVH::Bastion;
use OVH::Result;
local $| = 1;
#
# Globals
#
$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/pkg/bin';
my ($self) = $ENV{'SUDO_USER'} =~ m{^([a-zA-Z0-9._-]+)$};
if (not defined $self) {
if ($< == 0) {
$self = 'root';
}
else {
HEXIT('ERR_SUDO_NEEDED', msg => 'This command must be run under sudo');
}
}
# Fetch command options
Getopt::Long::Configure("no_auto_abbrev");
my $fnret;
my ($result, @optwarns);
my ($group, $mfaRequired, $ttl);
eval {
local $SIG{__WARN__} = sub { push @optwarns, shift };
$result = GetOptions(
"group=s" => sub { $group //= $_[1] },
"mfa-required=s" => \$mfaRequired,
"guest-ttl-limit=i" => \$ttl,
);
};
if ($@) { die $@ }
if (!$result) {
local $" = ", ";
HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns");
}
if (!$group) {
HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'group'");
}
if (!$mfaRequired && !defined $ttl) {
HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'mfa-required' or 'guest-ttl-limit'");
}
#<HEADER
#>PARAMS:ACCOUNT
$fnret = OVH::Bastion::is_valid_group_and_existing(group => $group, groupType => "key");
$fnret or HEXIT($fnret);
# get returned untainted value
$group = $fnret->value->{'group'};
my $shortGroup = $fnret->value->{'shortGroup'};
#<PARAMS:ACCOUNT
#>RIGHTSCHECK
if ($self eq 'root') {
osh_debug "Real root, skipping checks of permissions";
}
$fnret = OVH::Bastion::is_group_owner(account => $self, group => $shortGroup, superowner => 1, sudo => 1);
if (!$fnret) {
HEXIT('ERR_SECURITY_VIOLATION', msg => "You're not allowed to run this, dear $self");
}
#<RIGHTSCHECK
#>CODE
my %result;
if (defined $mfaRequired) {
osh_info "Modifying mfa-required policy of group...";
if (grep { $mfaRequired eq $_ } qw{ password totp any none }) {
$fnret = OVH::Bastion::group_config(group => $group, key => "mfa_required", value => $mfaRequired);
if ($fnret) {
osh_info "... done, policy is now: $mfaRequired";
}
else {
osh_warn "... error while changing mfa-required policy (" . $fnret->msg . ")";
}
$result{'mfa_required'} = $fnret;
}
else {
osh_warn "... invalid option '$mfaRequired'";
$result{'mfa_required'} = R('ERR_INVALID_PARAMETER');
}
}
if (defined $ttl) {
osh_info "Modifying guest TTL limit policy of group...";
if ($ttl > 0) {
$fnret = OVH::Bastion::group_config(group => $group, key => "guest_ttl_limit", value => $ttl);
if ($fnret) {
osh_info "... done, guest accesses must now have a TTL set on creation, with maximum allowed duration of "
. OVH::Bastion::duration2human(seconds => $ttl)->value->{'human'};
}
else {
osh_warn "... error while setting guest-ttl-limit (" . $fnret->msg . ")";
}
}
else {
$fnret = OVH::Bastion::group_config(group => $group, key => "guest_ttl_limit", delete => 1);
if ($fnret) {
osh_info "... done, guest accesses no longer need to have a TTL set";
}
else {
osh_warn "... error while removing guest-ttl-limit (" . $fnret->msg . ")";
}
}
$result{'guest_ttl_limit'} = $fnret;
}
HEXIT('OK', value => \%result);

140
bin/helper/osh-groupSetRole Executable file
View file

@ -0,0 +1,140 @@
#! /usr/bin/perl -T
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
# KEYSUDOERS # as an owner, we can grant/revoke ownership
# KEYSUDOERS SUPEROWNERS, %%GROUP%-owner ALL=(root) NOPASSWD: /usr/bin/env perl -T %BASEPATH%/bin/helper/osh-groupSetRole --type owner --group %GROUP% *
# KEYSUDOERS # as an owner, we can grant/revoke gatekeepership
# KEYSUDOERS SUPEROWNERS, %%GROUP%-owner ALL=(root) NOPASSWD: /usr/bin/env perl -T %BASEPATH%/bin/helper/osh-groupSetRole --type gatekeeper --group %GROUP% *
# KEYSUDOERS # as an owner, we can grant/revoke aclkeepership
# KEYSUDOERS SUPEROWNERS, %%GROUP%-owner ALL=(root) NOPASSWD: /usr/bin/env perl -T %BASEPATH%/bin/helper/osh-groupSetRole --type aclkeeper --group %GROUP% *
# KEYSUDOERS # as a gatekeeper, we can grant/revoke membership
# KEYSUDOERS SUPEROWNERS, %%GROUP%-gatekeeper ALL=(root) NOPASSWD: /usr/bin/env perl -T %BASEPATH%/bin/helper/osh-groupSetRole --type member --group %GROUP% *
# KEYSUDOERS # as a gatekeeper, we can grant/revoke a guest access
# KEYSUDOERS SUPEROWNERS, %%GROUP%-gatekeeper ALL=(root) NOPASSWD: /usr/bin/env perl -T %BASEPATH%/bin/helper/osh-groupSetRole --type guest --group %GROUP% *
# FILEMODE 0700
# FILEOWN root root
#>HEADER
use common::sense;
use Getopt::Long;
use File::Basename;
use lib dirname(__FILE__) . '/../../lib/perl';
use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin::groupSetRole;
local $| = 1;
#
# Globals
#
$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/pkg/bin';
my ($self) = $ENV{'SUDO_USER'} =~ m{^([a-zA-Z0-9._-]+)$};
if (not defined $self) {
if ($< == 0) {
$self = 'root';
}
else {
HEXIT('ERR_SUDO_NEEDED', msg => 'This command must be run under sudo');
}
}
# Fetch command options
Getopt::Long::Configure("no_auto_abbrev");
my $fnret;
my ($result, @optwarns);
my ($account, $group, $action, $type);
eval {
local $SIG{__WARN__} = sub { push @optwarns, shift };
$result = GetOptions(
"type=s" => sub { $type //= $_[1] },
"action=s" => sub { $action //= $_[1] },
"group=s" => sub { $group //= $_[1] }, # ignore subsequent --group on cmdline (anti-sudoers-override)
"account=s" => sub { $account //= $_[1] },
);
};
if ($@) { die $@ }
if (!$result) {
local $" = ", ";
HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns");
}
osh_debug("groupSetRole: checking preconditions");
$fnret = OVH::Bastion::Plugin::groupSetRole::preconditions(self => $self, account => $account, group => $group, action => $action, type => $type, sudo => 1, silentoverride => 1);
osh_debug("groupSetRole: checking preconditions result: $fnret");
$fnret or HEXIT($fnret);
my $shortGroup;
my %values = %{$fnret->value()};
($group, $shortGroup, $account, $type) = @values{qw{ group shortGroup account type }};
my ($sysaccount, $realm, $remoteaccount) = @values{qw{ sysaccount realm remoteaccount }};
#<PARAMS:GROUP
#>RIGHTSCHECK
#done in Plugin::groupSetRole::preconditions
#<RIGHTSCHECK
#>CODE
if ($type eq 'owner') {
$fnret = OVH::Bastion::is_group_owner(account => $account, group => $shortGroup, sudo => 1);
}
elsif ($type eq 'gatekeeper') {
$fnret = OVH::Bastion::is_group_gatekeeper(account => $account, group => $shortGroup, sudo => 1);
}
elsif ($type eq 'aclkeeper') {
$fnret = OVH::Bastion::is_group_aclkeeper(account => $account, group => $shortGroup, sudo => 1);
}
elsif ($type eq 'member') {
$fnret = OVH::Bastion::is_group_member(account => $account, group => $shortGroup, sudo => 1);
}
elsif ($type eq 'guest') {
$fnret = OVH::Bastion::is_group_guest(account => $account, group => $shortGroup, sudo => 1);
}
$fnret->is_err and HEXIT($fnret);
if ($action eq 'add' && $fnret->is_ok) {
osh_debug("groupSetRole: Account $account was already a $type of group $shortGroup, nothing to do");
HEXIT('OK_NO_CHANGE', msg => "Account $account was already a $type of group $shortGroup, nothing to do");
}
elsif ($action eq 'del' && $fnret->is_ko) {
osh_debug("groupSetRole: Account $account was not a $type of group $shortGroup, nothing to do");
HEXIT('OK_NO_CHANGE', msg => "Account $account was not a $type of group $shortGroup, nothing to do");
}
# add/del from sysgroup
my $groupName = ((grep { $type eq $_ } qw{ guest member }) ? $group : "$group-$type");
osh_debug("going to $action account $account to/from $groupName");
$fnret = R('OK', silent => 1);
if ($action eq 'add') {
if (!OVH::Bastion::is_user_in_group(user => $sysaccount, group => $groupName)) {
$fnret = OVH::Bastion::sys_addmembertogroup(group => $groupName, user => $sysaccount, noisy_stderr => 1);
}
}
elsif ($action eq 'del') {
# for realms, maybe we must not delete the shared realm account from the group, if other remote users are still members
my $otherMembers = 0;
if ($realm) {
$fnret = OVH::Bastion::get_remote_accounts_from_realm(realm => $realm);
$fnret or HEXIT($fnret);
foreach my $pRemoteaccount (@{$fnret->value}) {
next if ($pRemoteaccount eq $remoteaccount);
$otherMembers++ if OVH::Bastion::is_group_member(account => "$realm/$pRemoteaccount", group => $shortGroup, sudo => 1);
}
}
if (!$otherMembers) {
$fnret = OVH::Bastion::sys_delmemberfromgroup(group => $groupName, user => $sysaccount, noisy_stderr => 1);
}
}
else {
HEXIT('ERR_INTERNAL'); # unreachable
}
if ($fnret->err ne 'OK') {
osh_debug('Unable to modify group: ' . $fnret->msg);
HEXIT('ERR_INTERNAL', msg => "Error while doing $action on account $account from $type list of $shortGroup");
}
osh_debug("groupSetRole: Account $action of $account done on $type list of $shortGroup");
HEXIT('OK', msg => "Account $action of $account done on $type list of $shortGroup");

View file

@ -0,0 +1,112 @@
#! /usr/bin/perl -T
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
# FILEMODE 0700
# FILEOWN root root
#>HEADER
use common::sense;
use Getopt::Long;
use File::Basename;
use lib dirname(__FILE__) . '/../../lib/perl';
use OVH::Result;
use OVH::Bastion;
local $| = 1;
#
# Globals
#
$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/pkg/bin';
my ($self) = $ENV{'SUDO_USER'} =~ m{^([a-zA-Z0-9._-]+)$};
if (not defined $self) {
if ($< == 0) {
$self = 'root';
}
else {
HEXIT('ERR_SUDO_NEEDED', msg => 'This command must be run under sudo');
}
}
# Fetch command options
my $fnret;
my ($result, @optwarns);
my ($account, $step);
eval {
local $SIG{__WARN__} = sub { push @optwarns, shift };
$result = GetOptions(
"account=s" => sub { $account //= $_[1] },
"step=i" => sub { $step //= $_[1] },
);
};
if ($@) { die $@ }
if (!$result) {
local $" = ", ";
HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns");
}
if (!$account || !defined $step) {
HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'account' or 'step'");
}
#>RIGHTSCHECK
if ($self eq 'root') {
osh_debug "Real root, skipping checks of permissions";
}
elsif ($self ne $account) {
HEXIT('ERR_SECURITY_VIOLATION', msg => "You're not allowed to run this, dear $self");
}
#<RIGHTSCHECK
#>PARAMS:ACCOUNT
osh_debug("Checking account");
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account);
$fnret or HEXIT($fnret);
$account = $fnret->value->{'account'}; # untainted
#<PARAMS:ACCOUNT
if ($step == 0) {
# get password status
HEXIT(OVH::Bastion::sys_getpasswordinfo(user => $account));
}
elsif ($step == 1) {
# set a temporary password
my $password = sprintf("%04d-%04d-%04d-%04d", rand(10000), rand(10000), rand(10000), rand(10000));
$fnret = OVH::Bastion::sys_changepassword(user => $account, password => $password);
$fnret or HEXIT($fnret);
# force password change in 1 day max (it should be done several seconds after anyway)
$fnret = OVH::Bastion::sys_setpasswordpolicy(
user => $account,
inactiveDays => OVH::Bastion::config('MFAPasswordInactiveDays')->value,
minDays => 0,
maxDays => 1,
warnDays => 1
);
$fnret or HEXIT($fnret);
HEXIT('OK', value => {password => $password});
}
elsif ($step == 2) {
$fnret = OVH::Bastion::sys_addmembertogroup(user => $account, group => OVH::Bastion::MFA_PASSWORD_CONFIGURED_GROUP);
$fnret or HEXIT($fnret);
# now that we have the final password set, apply the bastionwide password policy
$fnret = OVH::Bastion::sys_setpasswordpolicy(
user => $account,
inactiveDays => OVH::Bastion::config('MFAPasswordInactiveDays')->value,
minDays => OVH::Bastion::config('MFAPasswordMinDays')->value,
maxDays => OVH::Bastion::config('MFAPasswordMaxDays')->value,
warnDays => OVH::Bastion::config('MFAPasswordWarnDays')->value
);
$fnret or HEXIT($fnret);
HEXIT('OK');
}
HEXIT('ERR_INVALID_PARAMETER', msg => "Parameter --step expects 0, 1 or 2");

71
bin/helper/osh-selfMFASetupTOTP Executable file
View file

@ -0,0 +1,71 @@
#! /usr/bin/perl -T
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
# FILEMODE 0700
# FILEOWN root root
#>HEADER
use common::sense;
use Getopt::Long;
use File::Copy;
use File::Basename;
use lib dirname(__FILE__) . '/../../lib/perl';
use OVH::Result;
use OVH::Bastion;
local $| = 1;
#
# Globals
#
$ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/pkg/bin';
my ($self) = $ENV{'SUDO_USER'} =~ m{^([a-zA-Z0-9._-]+)$};
if (not defined $self) {
if ($< == 0) {
$self = 'root';
}
else {
HEXIT('ERR_SUDO_NEEDED', msg => 'This command must be run under sudo');
}
}
# Fetch command options
my $fnret;
my ($result, @optwarns);
my ($account);
eval {
local $SIG{__WARN__} = sub { push @optwarns, shift };
$result = GetOptions("account=s" => sub { $account //= $_[1] });
};
if ($@) { die $@ }
if (!$result) {
local $" = ", ";
HEXIT('ERR_BAD_OPTIONS', msg => "Error parsing options: @optwarns");
}
if (not $account) {
HEXIT('ERR_MISSING_PARAMETER', msg => "Missing argument 'account'");
}
#>RIGHTSCHECK
if ($self eq 'root') {
osh_debug "Real root, skipping checks of permissions";
}
elsif ($self ne $account) {
HEXIT('ERR_SECURITY_VIOLATION', msg => "You're not allowed to run this, dear $self");
}
#<RIGHTSCHECK
#>PARAMS:ACCOUNT
osh_debug("Checking account");
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account);
$fnret or HEXIT($fnret);
$account = $fnret->value->{'account'}; # untainted
#<PARAMS:ACCOUNT
$fnret = OVH::Bastion::sys_addmembertogroup(user => $account, group => OVH::Bastion::MFA_TOTP_CONFIGURED_GROUP);
$fnret or HEXIT($fnret);
HEXIT('OK');

View file

@ -0,0 +1,32 @@
#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;
# DO NOT USE THIS SCRIPT IN PRODUCTION!
# This is only used for the functional tests, it returns true for odd UIDs, false otherwise.
# If you think this is a good way of determining your users activeness, you might want to revise your security procedures.
use constant {
EXIT_ACTIVE => 0,
EXIT_INACTIVE => 1,
EXIT_UNKNOWN => 2,
EXIT_UNKNOWN_SILENT_ERROR => 3,
EXIT_UNKNOWN_NOISY_ERROR => 4,
};
sub failtest {
my $msg = shift || "Error";
print STDERR "$msg. This will fail the test: MAKETESTFAIL\n";
exit EXIT_UNKNOWN_NOISY_ERROR;
}
my $sysaccount = shift;
if (!$sysaccount) {
failtest("No account name to check");
}
my $uid = getpwnam($sysaccount);
failtest("Can't find this account") if not defined $uid;
exit EXIT_ACTIVE if ($uid % 2 == 0);
exit EXIT_INACTIVE;

View file

@ -0,0 +1,54 @@
#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;
# This is an basic script to check whether an account is active or not.
# It serves as an example of what such a script can look like, but can also be used
# as is in production if it matches your use case.
# See the 'accountExternalValidationProgram' option in bastion.conf for more information
use constant {
EXIT_ACTIVE => 0,
EXIT_INACTIVE => 1,
EXIT_UNKNOWN => 2,
EXIT_UNKNOWN_SILENT_ERROR => 3,
EXIT_UNKNOWN_NOISY_ERROR => 4,
};
my $sysaccount = shift;
if (!$sysaccount) {
print STDERR "No account name to check. Report this to sysadmin!\n";
exit EXIT_UNKNOWN_NOISY_ERROR;
}
# This file should be a simple plaintext file containing one account name per line
# It should be populated by e.g. a cron script that queries some external directory
# such as an LDAP for example.
# Ensure that this file is readable at least by the bastion-users system group!
my $file = '/home/allowkeeper/active_accounts.txt';
if (!(-e $file)) {
print STDERR "Active accounts file is not present. Report this to sysadmin!\n";
exit EXIT_UNKNOWN_NOISY_ERROR;
}
# Load file
my $f;
if (!(open $f, '<', $file)) {
print STDERR "Active logins file is unreadable ($!). Report this to sysadmin!\n";
exit EXIT_UNKNOWN_NOISY_ERROR;
}
# check that the account is present in the file
while (<$f>) {
chomp;
if ($_ eq $sysaccount) {
close($f);
exit EXIT_ACTIVE;
}
}
close($f);
# If not, account is inactive
exit EXIT_INACTIVE;

View file

@ -0,0 +1,46 @@
#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;
use Term::ANSIColor;
use File::Basename;
use lib dirname(__FILE__) . '/../../../lib/perl';
use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT help );
my $remainingOptions = OVH::Bastion::Plugin::begin(
argv => \@ARGV,
header => "maintenance",
options => {
"lock" => \my $lock,
"unlock" => \my $unlock,
"message=s" => \my $message,
},
helptext => <<'EOF',
Manage the bastion maintenance mode
Usage: --osh SCRIPT_NAME <--lock [--message "'reason for maintenance'"]|--unlock>
--lock Set maintenance mode: new logins will be disallowed
--unlock Unset maintenance mode: new logins are allowed and the bastion functions normally
--message MESSAGE Optionally set a maintenance reason, if you're in a shell, quote it twice.
EOF
);
if (!$lock && !$unlock) {
help();
osh_exit 'ERR_MISSING_PARAMETER', "Expected --lock or --unlock";
}
if ($lock && $unlock) {
help();
osh_exit 'ERR_INCOMPATIBLE_PARAMETERS', "Got both --lock and --unlock, what are your trying to do exactly?";
}
my @command = qw{ sudo -n -u allowkeeper -- /usr/bin/env perl -T };
push @command, $OVH::Bastion::BASEPATH . '/bin/helper/osh-adminMaintenance';
push @command, "--action", ($lock ? 'set' : 'unset');
push @command, "--message", $message if $message;
osh_exit OVH::Bastion::helper(cmd => \@command);

View file

@ -0,0 +1,9 @@
{
"interactive": [
"adminMaintenance" , { "ac" : ["--lock","--unlock"]},
"adminMaintenance --lock" , { "ac" : ["--message","<enter>"]},
"adminMaintenance --lock --message" , { "pr" : ["\"<MESSAGE>\""]},
"adminMaintenance --lock --message .+" , { "pr" : ["<enter>"]},
"adminMaintenance --unlock" , { "pr" : ["<enter>"]}
]
}

73
bin/plugin/admin/adminSudo Executable file
View file

@ -0,0 +1,73 @@
#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;
use File::Basename;
use lib dirname(__FILE__) . '/../../../lib/perl';
use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT help );
my $remainingOptions = OVH::Bastion::Plugin::begin(
argv => \@ARGV,
header => "launching a bastion command or connection, impersonating another user",
options => {
"sudo-as=s" => \my $sudoAs,
"sudo-cmd=s" => \my $sudoCmd,
},
helptext => <<'EOF',
Impersonate another user
Usage: --osh SCRIPT_NAME -- --sudo-as ACCOUNT <--sudo-cmd PLUGIN -- [PLUGIN specific options...]>
--sudo-as ACCOUNT Specify which bastion account we want to impersonate
--sudo-cmd PLUGIN --osh command we want to launch as the user (see --osh help)
Example::
--osh SCRIPT_NAME -- --sudo-as user12 --sudo-cmd info -- --name somebodyelse
Don't forget the double-double-dash as seen in the example above: one after the plugin name,
and another one to separate SCRIPT_NAME options from the options of the plugin to be called.
EOF
);
my $fnret;
if (not $sudoAs or not $sudoCmd) {
help();
osh_exit 'ERR_MISSING_PARAMETER', "Missing mandatory parameter 'sudo-as' or 'sudo-cmd'";
}
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $sudoAs);
$fnret or osh_exit($fnret);
$fnret = OVH::Bastion::can_account_execute_plugin(account => $sudoAs, plugin => $sudoCmd);
$fnret or osh_exit($fnret);
my @cmd = qw( sudo -n -u );
push @cmd, $sudoAs;
push @cmd, qw( -- /usr/bin/env perl );
push @cmd, $OVH::Bastion::BASEPATH . '/bin/shell/osh.pl';
push @cmd, '-c';
my $stringified;
$stringified = " --osh $sudoCmd" if $sudoCmd;
$stringified .= " --host $host" if $host;
$stringified .= " --port $port" if $port;
$stringified .= " --user $user" if $user;
$stringified .= " " . join(" ", @$remainingOptions) if ($remainingOptions and @$remainingOptions);
push @cmd, $stringified;
OVH::Bastion::syslogFormatted(
criticity => 'info',
type => 'security',
fields => [['type', 'admin-sudo'], ['account', $self], ['sudo-as', $sudoAs], ['plugin', ($sudoCmd ? $sudoCmd : 'ssh')], ['params', $stringified]]
);
osh_warn("ADMIN SUDO: $self, you'll now impersonate $sudoAs, this has been logged.");
$fnret = OVH::Bastion::execute(cmd => \@cmd, noisy_stdout => 1, noisy_stderr => 1);
osh_exit $fnret;

View file

@ -0,0 +1,8 @@
{
"interactive": [
"adminSudo" , { "ac" : ["-- --sudo-as"]},
"adminSudo -- --sudo-as" , { "ac" : ["<ACCOUNT>" ]},
"adminSudo -- --sudo-as \\S+" , { "ac" : ["--sudo-cmd" ]},
"adminSudo -- --sudo-as \\S+ --sudo-cmd" , { "pr" : ["<PLUGIN> -- <ARG1> <ARG2>" ]}
]
}

View file

@ -0,0 +1,144 @@
#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;
use File::Basename;
use lib dirname(__FILE__) . '/../../../lib/perl';
use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT help );
my $remainingOptions = OVH::Bastion::Plugin::begin(
argv => \@ARGV,
header => "adding a server to a group",
options => {
"group=s" => \my $group,
"user-any" => \my $userAny,
"port-any" => \my $portAny,
"scpup" => \my $scpUp,
"scpdown" => \my $scpDown,
"force" => \my $force, # for slashes, and/or for servers that are down (no connection test)
"force-key" => \my $forceKey,
"ttl=s" => \my $ttl,
"comment=s" => \my $comment,
},
helptext => <<'EOF',
Add an IP or IP block to a group's servers list
Usage: --osh SCRIPT_NAME --group GROUP [OPTIONS]
--group GROUP Specify which group this machine should be added to (it should have the public group key of course)
--host HOST|IP|NET/CIDR Host(s) to add access to, either a HOST which will be resolved to an IP immediately, or an IP,
or a whole network using the NET/CIDR notation
--user USER Specify which remote user should be allowed (root, run, etc...)
--user-any Allow any remote user (the remote user should still have the public group key in all cases)
--port PORT Only allow access to this port (e.g. 22)
--port-any Allow access to any port
--scpup Allow SCP upload, you--bastion-->server (omit --user in this case)
--scpdown Allow SCP download, you<--bastion--server (omit --user in this case)
--force Don't try the ssh connection, just add the host to the group blindly
--force-key FINGERPRINT Only use the key with the specified fingerprint to connect to the server (cf groupInfo)
--ttl SECONDS|DURATION Specify a number of seconds (or a duration string, such as "1d7h8m") after which the access will automatically expire
--comment '"ANY TEXT'" Add a comment alongside this server
Examples::
--osh SCRIPT_NAME --group grp1 --host 203.0.113.0/24 --user-any --port-any --force --comment '"a whole network"'
--osh SCRIPT_NAME --group grp2 --host srv1.example.org --user root --port 22
EOF
);
my $fnret;
if (not $group or not $ip) {
help();
osh_exit 'ERR_MISSING_PARAMETER', "Missing mandatory parameter 'host' or 'group' (or host didn't resolve correctly)";
}
if ($user and $userAny) {
help();
osh_exit 'ERR_INCOMPATIBLE_PARAMETERS', "A user was specified, but --user-any used, these are incompatible, please think about what you're doing";
}
if ($scpUp and $scpDown) {
help();
osh_exit 'ERR_INCOMPATIBLE_PARAMETERS', "You specified both --scpup and --scpdown, if you want to grant both, please do it in two separate commands";
}
if (($scpUp or $scpDown) and ($user or $userAny)) {
help();
osh_exit 'ERR_INCOMPATIBLE_PARAMETERS',
"To grant SCP access, first ensure SSH access is granted to the machine (with the --user you need, or --user-any), then grant with --scpup and/or --scpdown, omitting --user/--user-any";
}
$user = '!scpupload' if $scpUp;
$user = '!scpdownload' if $scpDown;
if (not $user and not $userAny) {
help();
osh_exit 'ERR_MISSING_PARAMETER', "No user specified, if you want to add this server with any user, add --user-any";
}
if ($portAny and $port) {
help();
osh_exit 'ERR_INCOMPATIBLE_PARAMETERS', "A port was specified, but --port-any used, these are incompatible, please think about what you're doing";
}
if (not $port and not $portAny) {
help();
osh_exit 'ERR_MISSING_PARAMETER', "No port specified, if you want to add this server with any port, add --port-any";
}
$fnret = OVH::Bastion::is_valid_group_and_existing(group => $group, groupType => "key");
$fnret or osh_exit($fnret);
# get returned untainted value
$group = $fnret->value->{'group'};
my $shortGroup = $fnret->value->{'shortGroup'};
if (defined $ttl) {
$fnret = OVH::Bastion::is_valid_ttl(ttl => $ttl);
$fnret or osh_exit $fnret;
$ttl = $fnret->value->{'seconds'};
}
if ($forceKey) {
$fnret = OVH::Bastion::is_valid_fingerprint(fingerprint => $forceKey);
$fnret or osh_exit $fnret;
$forceKey = $fnret->value->{'fingerprint'};
}
#
# Now do it
#
$fnret = OVH::Bastion::is_group_aclkeeper(account => $self, group => $shortGroup, superowner => 1);
$fnret or osh_exit 'ERR_NOT_GROUP_ACLKEEPER', "Sorry, you must be an aclkeeper of group $shortGroup to be able to add servers to it";
if (not $force) {
$fnret = OVH::Bastion::ssh_test_access_way(group => $group, user => $user, port => $port, ip => $ip, forceKey => $forceKey);
if ($fnret->is_ok and $fnret->err ne 'OK') {
# we have something to say, say it
osh_info $fnret->msg;
}
elsif (not $fnret) {
osh_info "Note: if you still want to add this access even if it doesn't work, use --force";
osh_exit $fnret;
}
}
else {
osh_info "Forcing add as asked, we didn't test the SSH connection, maybe it won't work!";
}
my @command = qw{ sudo -n -u };
push @command, ($group, '--', '/usr/bin/env', 'perl', '-T', $OVH::Bastion::BASEPATH . '/bin/helper/osh-groupAddServer');
push @command, '--group', $group;
push @command, '--action', 'add';
push @command, '--ip', $ip;
push @command, '--user', $user if $user;
push @command, '--port', $port if $port;
push @command, '--force-key', $forceKey if $forceKey;
push @command, '--ttl', $ttl if $ttl;
push @command, '--comment', $comment if $comment;
osh_exit OVH::Bastion::helper(cmd => \@command);

View file

@ -0,0 +1,14 @@
{
"interactive": [
"groupAddServer" , {"ac" : ["--group"]},
"groupAddServer --group" , {"ac" : ["<GROUP>"]},
"groupAddServer --group \\S+" , {"ac" : ["--host"]},
"groupAddServer --group \\S+ --host" , {"pr" : ["<HOST>", "<IP>", "<IP/MASK>"]},
"groupAddServer --group \\S+ --host \\S+" , {"ac" : ["--port", "--port-any"]},
"groupAddServer --group \\S+ --host \\S+ --port" , {"pr" : ["<PORT>"]},
"groupAddServer --group \\S+ --host \\S+ --port(-any| \\d+)" , {"ac" : ["--user", "--user-any"]},
"groupAddServer --group \\S+ --host \\S+ --port(-any| \\d+) --user" , {"pr" : ["<USER>"]},
"groupAddServer --group \\S+ --host \\S+ --port(-any| \\d+) --user(-any| \\S+)" , {"pr" : ["<enter>", "--force"]}
],
"master_only": true
}

View file

@ -0,0 +1,100 @@
#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;
use File::Basename;
use lib dirname(__FILE__) . '/../../../lib/perl';
use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT help );
my $remainingOptions = OVH::Bastion::Plugin::begin(
argv => \@ARGV,
header => "removing a server from a group",
options => {
"group=s" => \my $group,
"user-any" => \my $userAny,
"port-any" => \my $portAny,
"scpup" => \my $scpUp,
"scpdown" => \my $scpDown,
"force" => \my $force,
},
helptext => <<'EOF',
Remove an IP or IP block from a group's serrver list
Usage: --osh SCRIPT_NAME --group GROUP [OPTIONS]
--group GROUP Specify which group this machine should be removed from
--host HOST|IP|NET/CIDR Host(s) we want to remove access to
--user USER Remote user that was allowed, if any user was allowed, use --user-any
--user-any Use if any remote login was allowed
--port PORT Remote SSH port that was allowed, if any port was allowed, use --port-any
--port-any Use if any remote port was allowed
--scpup Remove SCP upload right, you--bastion-->server (omit --user in this case)
--scpdown Remove SCP download right, you<--bastion--server (omit --user in this case)
EOF
);
my $fnret;
if (not $group or not $ip) {
help();
osh_exit 'ERR_MISSING_PARAMETER', "Missing mandatory parameter 'host' or 'group' (or host didn't resolve correctly)";
}
if ($user and $userAny) {
help();
osh_exit 'ERR_INCOMPATIBLE_PARAMETERS', "A user was specified, but --user-any used, these are incompatible, please think about what you're doing";
}
if ($scpUp and $scpDown) {
help();
osh_exit 'ERR_INCOMPATIBLE_PARAMETERS', "You specified both --scpup and --scpdown, if you want to grant both, please do it in two separate commands";
}
if (($scpUp or $scpDown) and ($user or $userAny)) {
help();
osh_exit 'ERR_INCOMPATIBLE_PARAMETERS',
"To grant SCP access, first ensure SSH access is granted to the machine (with the --user you need, or --user-any), then grant with --scpup and/or --scpdown, omitting --user/--user-any";
}
$user = '!scpupload' if $scpUp;
$user = '!scpdownload' if $scpDown;
if (not $user and not $userAny) {
help();
osh_exit 'ERR_MISSING_PARAMETER', "No user specified, if you want to add this server with any user, add --user-any";
}
if ($portAny and $port) {
help();
osh_exit 'ERR_INCOMPATIBLE_PARAMETERS', "A port was specified, but --port-any used, these are incompatible, please think about what you're doing";
}
if (not $port and not $portAny) {
help();
osh_exit 'ERR_MISSING_PARAMETER', "No port specified, if you want to add this server with any port, add --port-any";
}
$fnret = OVH::Bastion::is_valid_group_and_existing(group => $group, groupType => "key");
$fnret or osh_exit($fnret);
# get returned untainted value
$group = $fnret->value->{'group'};
my $shortGroup = $fnret->value->{'shortGroup'};
#
# Now do it
#
$fnret = OVH::Bastion::is_group_aclkeeper(account => $self, group => $shortGroup, superowner => 1);
$fnret or osh_exit 'ERR_NOT_GROUP_ACLKEEPER', "Sorry, you must be an aclkeeper of group $shortGroup to be able to delete servers from it";
my @command = qw{ sudo -n -u };
push @command, ($group, '--', '/usr/bin/env', 'perl', '-T', $OVH::Bastion::BASEPATH . '/bin/helper/osh-groupAddServer');
push @command, '--group', $group;
push @command, '--action', 'del';
push @command, '--ip', $ip;
push @command, '--user', $user if $user;
push @command, '--port', $port if $port;
osh_exit OVH::Bastion::helper(cmd => \@command);

View file

@ -0,0 +1,14 @@
{
"interactive": [
"groupAddServer" , {"ac" : ["--group"]},
"groupAddServer --group" , {"ac" : ["<GROUP>"]},
"groupAddServer --group \\S+" , {"ac" : ["--host"]},
"groupAddServer --group \\S+ --host" , {"pr" : ["<HOST>", "<IP>", "<IP/MASK>"]},
"groupAddServer --group \\S+ --host \\S+" , {"ac" : ["--port", "--port-any"]},
"groupAddServer --group \\S+ --host \\S+ --port" , {"pr" : ["<PORT>"]},
"groupAddServer --group \\S+ --host \\S+ --port(-any| \\d+)" , {"ac" : ["--user", "--user-any"]},
"groupAddServer --group \\S+ --host \\S+ --port(-any| \\d+) --user" , {"pr" : ["<USER>"]},
"groupAddServer --group \\S+ --host \\S+ --port(-any| \\d+) --user(-any| \\S+)" , {"pr" : ["<enter>", "--force"]}
],
"master_only": true
}

View file

@ -0,0 +1,93 @@
#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;
use File::Basename;
use lib dirname(__FILE__) . '/../../../lib/perl';
use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT help );
use OVH::Bastion::Plugin::groupSetRole;
my $remainingOptions = OVH::Bastion::Plugin::begin(
argv => \@ARGV,
header => "add access to one server of a group to an account",
options => {
"group=s" => \my $group,
"account=s" => \my $account,
"user-any" => \my $userAny,
"port-any" => \my $portAny,
"scpup" => \my $scpUp,
"scpdown" => \my $scpDown,
"ttl=s" => \my $ttl,
},
helptext => <<'EOF',
Add a specific group server access to an account
Usage: --osh SCRIPT_NAME --group GROUP --account ACCOUNT [OPTIONS]
--group GROUP group to add guest access to
--account ACCOUNT name of the other bastion account to add access to, he'll be given access to the GROUP key
--host HOST|IP add access to this HOST (which must belong to the GROUP)
--user USER allow connecting to HOST only with remote login USER
--user-any allow connecting to HOST with any remote login
--port PORT allow connecting to HOST only to remote port PORT
--port-any allow connecting to HOST with any remote port
--scpup allow SCP upload, you--bastion-->server (omit --user in this case)
--scpdown allow SCP download, you<--bastion--server (omit --user in this case)
--ttl SECONDS|DURATION Specify a number of seconds after which the access will automatically expire
This command adds, to an existing bastion account, access to the egress keys of a group,
but only to accessing one or several given servers, instead of all the servers of this group.
If you want to add complete access to an account to all the present and future servers
of the group, using the group key, please use ``groupAddMember`` instead.
If you want to add access to an account to a group server but using his personal bastion
key instead of the group key, please use ``accountAddPersonalAccess`` instead (his public key
must be on the remote server).
This command is the opposite of ``groupDelGuestAccess``.
EOF
);
if (not $ip and $host) {
osh_exit 'ERR_INVALID_HOST', "Specified host ($host) didn't resolve correctly, fix your DNS or specify the IP instead";
}
if ($scpUp and $scpDown) {
help();
osh_exit 'ERR_INCOMPATIBLE_PARAMETERS', "You specified both --scpup and --scpdown, if you want to grant both, please do it in two separate commands";
}
if (($scpUp or $scpDown) and ($user or $userAny)) {
help();
osh_exit 'ERR_INCOMPATIBLE_PARAMETERS',
"To grant SCP access, first ensure SSH access is granted to the machine (with the --user you need, or --user-any), then grant with --scpup and/or --scpdown, omitting --user/--user-any";
}
if (defined $ttl) {
my $fnret = OVH::Bastion::is_valid_ttl(ttl => $ttl);
$fnret or osh_exit $fnret;
$ttl = $fnret->value->{'seconds'};
}
my $realUser = $user;
$realUser = '!scpupload' if $scpUp;
$realUser = '!scpdownload' if $scpDown;
my $fnret = OVH::Bastion::Plugin::groupSetRole::act(
account => $account,
group => $group,
action => 'add',
type => 'guest',
user => $realUser,
userAny => $userAny,
port => $port,
portAny => $portAny,
host => ($ip || $host),
ttl => $ttl,
sudo => 0,
silentoverride => 0,
self => $self,
scriptName => $scriptName,
savedArgs => $savedArgs
);
help() if not $fnret;
osh_exit($fnret);

View file

@ -0,0 +1,17 @@
{
"interactive": [
"groupAddGuestAccess" , {"ac" : ["--account"]},
"groupAddGuestAccess --account" , {"ac" : ["<ACCOUNT>"]},
"groupAddGuestAccess --account \\S+" , {"ac" : ["--group"]},
"groupAddGuestAccess --account \\S+ --group" , {"ac" : ["<GROUP>"]},
"groupAddGuestAccess --account \\S+ --group \\S+" , {"ac" : ["--host"]},
"groupAddGuestAccess --account \\S+ --group \\S+ --host" , {"pr" : ["<HOST>", "<IP>", "<IP/MASK>"]},
"groupAddGuestAccess --account \\S+ --group \\S+ --host \\S+" , {"ac" : ["<enter>", "--user", "--port"]},
"groupAddGuestAccess --account \\S+ --group \\S+ --host \\S+ .*--user" , {"pr" : ["<USER>"]},
"groupAddGuestAccess --account \\S+ --group \\S+ --host \\S+ .*--port" , {"pr" : ["<PORT>"]},
"groupAddGuestAccess --account \\S+ --group \\S+ --host \\S+ --user \\S+" , {"ac" : ["<enter>", "--port"]},
"groupAddGuestAccess --account \\S+ --group \\S+ --host \\S+ --port \\S+" , {"ac" : ["<enter>", "--user"]},
"groupAddGuestAccess --account \\S+ --group \\S+ --host \\S+ --(port|user) \\S+ --(port|user) \\S+" , {"pr" : ["<enter>"]}
],
"master_only": true
}

View file

@ -0,0 +1,46 @@
#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;
use File::Basename;
use lib dirname(__FILE__) . '/../../../lib/perl';
use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT help );
use OVH::Bastion::Plugin::groupSetRole;
my $remainingOptions = OVH::Bastion::Plugin::begin(
argv => \@ARGV,
header => "grant an account as member of a group",
options => {
"account=s" => \my $account,
"group=s" => \my $group,
},
helptext => <<'EOF',
Add an account to the member list
Usage: --osh SCRIPT_NAME --group GROUP --account ACCOUNT
--group GROUP which group to set ACCOUNT as a member of
--account ACCOUNT which account to set as a member of GROUP
The specified account will be able to access all present and future servers
pertaining to this group.
If you need to give a specific and/or temporary access instead,
see ``groupAddGuestAccess``
EOF
);
my $fnret = OVH::Bastion::Plugin::groupSetRole::act(
account => $account,
group => $group,
action => 'add',
type => 'member',
sudo => 0,
silentoverride => 0,
self => $self,
scriptName => $scriptName,
savedArgs => $savedArgs
);
help() if not $fnret;
osh_exit($fnret);

View file

@ -0,0 +1,10 @@
{
"interactive": [
"groupAddMember" , {"ac" : ["--account"]},
"groupAddMember --account" , {"ac" : ["<ACCOUNT>"]},
"groupAddMember --account \\S+" , {"ac" : ["--group"]},
"groupAddMember --account \\S+ --group" , {"ac" : ["<GROUP>"]},
"groupAddMember --account \\S+ --group \\S+" , {"pr" : ["<enter>"]}
],
"master_only": true
}

View file

@ -0,0 +1,84 @@
#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;
use File::Basename;
use lib dirname(__FILE__) . '/../../../lib/perl';
use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT help );
use OVH::Bastion::Plugin::groupSetRole;
my $remainingOptions = OVH::Bastion::Plugin::begin(
argv => \@ARGV,
header => "remove access from one server of a group from an account",
options => {
"group=s" => \my $group,
"account=s" => \my $account,
"user-any" => \my $userAny,
"port-any" => \my $portAny,
"scpup" => \my $scpUp,
"scpdown" => \my $scpDown,
},
helptext => <<'EOF',
Remove a specific group server access from an account
Usage: --osh SCRIPT_NAME --group GROUP --account ACCOUNT [OPTIONS]
--group GROUP group to remove guest access from
--account ACCOUNT name of the other bastion account to remove access from
--host HOST|IP remove access from this HOST (which must belong to the GROUP)
--user USER allow connecting to HOST only with remote login USER
--user-any allow connecting to HOST with any remote login
--port PORT allow connecting to HOST only to remote port PORT
--port-any allow connecting to HOST with any remote port
--scpup allow SCP upload, you--bastion-->server (omit --user in this case)
--scpdown allow SCP download, you<--bastion--server (omit --user in this case)
This command removes, from an existing bastion account, access to a given server, using the
egress keys of the group. The list of such servers is given by ``groupListGuestAccesses``
If you want to remove member access from an account to all the present and future servers
of the group, using the group key, please use ``groupDelMember`` instead.
If you want to remove access from an account from a group server but using his personal bastion
key instead of the group key, please use ``accountDelPersonalAccess`` instead.
This command is the opposite of ``groupAddGuestAccess``.
EOF
);
if (not $ip and $host) {
osh_exit 'ERR_INVALID_HOST', "Specified host ($host) didn't resolve correctly, fix your DNS or specify the IP instead";
}
if ($scpUp and $scpDown) {
help();
osh_exit 'ERR_INCOMPATIBLE_PARAMETERS', "You specified both --scpup and --scpdown, if you want to grant both, please do it in two separate commands";
}
if (($scpUp or $scpDown) and ($user or $userAny)) {
help();
osh_exit 'ERR_INCOMPATIBLE_PARAMETERS',
"To grant SCP access, first ensure SSH access is granted to the machine (with the --user you need, or --user-any), then grant with --scpup and/or --scpdown, omitting --user/--user-any";
}
my $realUser = $user;
$realUser = '!scpupload' if $scpUp;
$realUser = '!scpdownload' if $scpDown;
my $fnret = OVH::Bastion::Plugin::groupSetRole::act(
account => $account,
group => $group,
action => 'del',
type => 'guest',
user => $realUser,
userAny => $userAny,
port => $port,
portAny => $portAny,
host => ($ip || $host),
sudo => 0,
silentoverride => 0,
self => $self,
scriptName => $scriptName,
savedArgs => $savedArgs
);
help() if not $fnret;
osh_exit($fnret);

View file

@ -0,0 +1,17 @@
{
"interactive": [
"groupDelGuestAccess" , {"ac" : ["--account"]},
"groupDelGuestAccess --account" , {"ac" : ["<ACCOUNT>"]},
"groupDelGuestAccess --account \\S+" , {"ac" : ["--group"]},
"groupDelGuestAccess --account \\S+ --group" , {"ac" : ["<GROUP>"]},
"groupDelGuestAccess --account \\S+ --group \\S+" , {"ac" : ["--host"]},
"groupDelGuestAccess --account \\S+ --group \\S+ --host" , {"pr" : ["<HOST>", "<IP>", "<IP/MASK>"]},
"groupDelGuestAccess --account \\S+ --group \\S+ --host \\S+" , {"ac" : ["<enter>", "--user", "--port"]},
"groupDelGuestAccess --account \\S+ --group \\S+ --host \\S+ .*--user" , {"pr" : ["<USER>"]},
"groupDelGuestAccess --account \\S+ --group \\S+ --host \\S+ .*--port" , {"pr" : ["<PORT>"]},
"groupDelGuestAccess --account \\S+ --group \\S+ --host \\S+ --user \\S+" , {"ac" : ["<enter>", "--port"]},
"groupDelGuestAccess --account \\S+ --group \\S+ --host \\S+ --port \\S+" , {"ac" : ["<enter>", "--user"]},
"groupDelGuestAccess --account \\S+ --group \\S+ --host \\S+ --(port|user) \\S+ --(port|user) \\S+" , {"pr" : ["<enter>"]}
],
"master_only": true
}

View file

@ -0,0 +1,46 @@
#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;
use File::Basename;
use lib dirname(__FILE__) . '/../../../lib/perl';
use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT help );
use OVH::Bastion::Plugin::groupSetRole;
my $remainingOptions = OVH::Bastion::Plugin::begin(
argv => \@ARGV,
header => "revoke an account as member of a group",
options => {
"account=s" => \my $account,
"group=s" => \my $group,
},
helptext => <<'EOF',
Remove an account from the members list
Usage: --osh SCRIPT_NAME --group GROUP --account ACCOUNT
--group GROUP which group to remove ACCOUNT as a member of
--account ACCOUNT which account to remove as a member of GROUP
The specified account will no longerr be able to access all present and future servers
pertaining to this group.
Note that if this account also had specific guest accesses to this group, they may
still apply, see ``groupListGuestAccesses``
EOF
);
my $fnret = OVH::Bastion::Plugin::groupSetRole::act(
account => $account,
group => $group,
action => 'del',
type => 'member',
sudo => 0,
silentoverride => 0,
self => $self,
scriptName => $scriptName,
savedArgs => $savedArgs
);
help() if not $fnret;
osh_exit($fnret);

View file

@ -0,0 +1,10 @@
{
"interactive": [
"groupDelMember" , {"ac" : ["--account"]},
"groupDelMember --account" , {"ac" : ["<ACCOUNT>"]},
"groupDelMember --account \\S+" , {"ac" : ["--group"]},
"groupDelMember --account \\S+ --group" , {"ac" : ["<GROUP>"]},
"groupDelMember --account \\S+ --group \\S+" , {"pr" : ["<enter>"]}
],
"master_only": true
}

View file

@ -0,0 +1,52 @@
#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;
use File::Basename;
use lib dirname(__FILE__) . '/../../../lib/perl';
use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT help );
my ($group, $account, $reverse);
my $remainingOptions = OVH::Bastion::Plugin::begin(
argv => \@ARGV,
header => "lists partial access to group servers of a bastion account",
options => {
"group=s" => \$group,
"account=s" => \$account,
"reverse-dns" => \$reverse,
},
helptext => <<'EOF',
List the guest accesses to servers of a group specifically granted to an account
Usage: --osh SCRIPT_NAME --group GROUP --account ACCOUNT
--group GROUP Look for accesses to servers of this GROUP
--account ACCOUNT Which account to check
EOF
);
my $fnret;
if (not $group or not $account) {
help();
osh_exit 'ERR_MISSING_PARAMETER', "Missing mandatory parameter 'account' or 'group'";
}
$fnret = OVH::Bastion::is_valid_group_and_existing(group => $group, groupType => "key");
$fnret or osh_exit $fnret;
# get returned untainted value
$group = $fnret->value->{'group'};
my $shortGroup = $fnret->value->{'shortGroup'};
$fnret = OVH::Bastion::get_acl_way(way => 'groupguest', group => $shortGroup, account => $account);
$fnret or osh_exit $fnret;
if (not @{$fnret->value}) {
osh_ok R('OK_EMPTY', msg => "This account doesn't seem to have any guest access to this group");
}
OVH::Bastion::print_acls(acls => [{type => 'group-guest', group => $shortGroup, acl => $fnret->value}], reverse => $reverse);
osh_ok($fnret->value);

View file

@ -0,0 +1,9 @@
{
"interactive": [
"groupListGuestAccesses" , {"ac" : ["--account"]},
"groupListGuestAccesses --account" , {"ac" : ["<ACCOUNT>"]},
"groupListGuestAccesses --account \\S+" , {"ac" : ["--group"]},
"groupListGuestAccesses --account \\S+ --group" , {"ac" : ["<GROUP>"]},
"groupListGuestAccesses --account \\S+ --group \\S+" , {"pr" : ["<enter>"]}
]
}

View file

@ -0,0 +1,40 @@
#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;
use File::Basename;
use lib dirname(__FILE__) . '/../../../lib/perl';
use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT help );
use OVH::Bastion::Plugin::groupSetRole;
my $remainingOptions = OVH::Bastion::Plugin::begin(
argv => \@ARGV,
header => "grant an account as aclkeeper of a group",
options => {"account=s" => \my $account, "group=s" => \my $group},
helptext => <<'EOF',
Add the group aclkeeper role to an account
Usage: --osh SCRIPT_NAME --group GROUP --account ACCOUNT
--group GROUP which group to set ACCOUNT as an aclkeeper of
--account ACCOUNT which account to set as an aclkeeper of GROUP
The specified account will be able to manage the server list of this group
EOF
);
my $fnret = OVH::Bastion::Plugin::groupSetRole::act(
account => $account,
group => $group,
action => 'add',
type => 'aclkeeper',
sudo => 0,
silentoverride => 0,
self => $self,
scriptName => $scriptName,
savedArgs => $savedArgs
);
help() if not $fnret;
osh_exit($fnret);

View file

@ -0,0 +1,10 @@
{
"interactive": [
"groupAddAclkeeper" , {"ac" : ["--account"]},
"groupAddAclkeeper --account" , {"ac" : ["<ACCOUNT>"]},
"groupAddAclkeeper --account \\S+" , {"ac" : ["--group"]},
"groupAddAclkeeper --account \\S+ --group" , {"ac" : ["<GROUP>"]},
"groupAddAclkeeper --account \\S+ --group \\S+" , {"pr" : ["<enter>"]}
],
"master_only": true
}

View file

@ -0,0 +1,42 @@
#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;
use File::Basename;
use lib dirname(__FILE__) . '/../../../lib/perl';
use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT help );
use OVH::Bastion::Plugin::groupSetRole;
my ($account, $group);
my $remainingOptions = OVH::Bastion::Plugin::begin(
argv => \@ARGV,
header => "grant an account as gatekeeper of a group",
options => {"account=s", \$account, "group=s", \$group},
helptext => <<'EOF',
Add the group gatekeeper role to an account
Usage: --osh SCRIPT_NAME --group GROUP --account ACCOUNT
--group GROUP which group to set ACCOUNT as a gatekeeper of
--account ACCOUNT which account to set as a gatekeeper of GROUP
The specified account will be able to manage the members list of this group,
along with the guests list
EOF
);
my $fnret = OVH::Bastion::Plugin::groupSetRole::act(
account => $account,
group => $group,
action => 'add',
type => 'gatekeeper',
sudo => 0,
silentoverride => 0,
self => $self,
scriptName => $scriptName,
savedArgs => $savedArgs
);
help() if not $fnret;
osh_exit($fnret);

View file

@ -0,0 +1,10 @@
{
"interactive": [
"groupAddGatekeeper" , {"ac" : ["--account"]},
"groupAddGatekeeper --account" , {"ac" : ["<ACCOUNT>"]},
"groupAddGatekeeper --account \\S+" , {"ac" : ["--group"]},
"groupAddGatekeeper --account \\S+ --group" , {"ac" : ["<GROUP>"]},
"groupAddGatekeeper --account \\S+ --group \\S+" , {"pr" : ["<enter>"]}
],
"master_only": true
}

View file

@ -0,0 +1,44 @@
#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;
use File::Basename;
use lib dirname(__FILE__) . '/../../../lib/perl';
use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT help );
use OVH::Bastion::Plugin::groupSetRole;
my ($account, $group);
my $remainingOptions = OVH::Bastion::Plugin::begin(
argv => \@ARGV,
header => "grant an account as owner of a group",
options => {"account=s", \$account, "group=s", \$group},
helptext => <<'EOF',
Add the group owner role to an account
Usage: --osh SCRIPT_NAME --group GROUP --account ACCOUNT
--group GROUP which group to set ACCOUNT as an owner of
--account ACCOUNT which account to set as an owner of GROUP
The specified account will be able to manage the owner, gatekeeper
and aclkeeper list of this group. In other words, this account will
have all possible rights to manage the group and delegate some or all
of the rights to other accounts
EOF
);
my $fnret = OVH::Bastion::Plugin::groupSetRole::act(
account => $account,
group => $group,
action => 'add',
type => 'owner',
sudo => 0,
silentoverride => 0,
self => $self,
scriptName => $scriptName,
savedArgs => $savedArgs
);
help() if not $fnret;
osh_exit($fnret);

View file

@ -0,0 +1,10 @@
{
"interactive": [
"groupAddOwner" , {"ac" : ["--account"]},
"groupAddOwner --account" , {"ac" : ["<ACCOUNT>"]},
"groupAddOwner --account \\S+" , {"ac" : ["--group"]},
"groupAddOwner --account \\S+ --group" , {"ac" : ["<GROUP>"]},
"groupAddOwner --account \\S+ --group \\S+" , {"pr" : ["<enter>"]}
],
"master_only": true
}

View file

@ -0,0 +1,40 @@
#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;
use File::Basename;
use lib dirname(__FILE__) . '/../../../lib/perl';
use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT help );
use OVH::Bastion::Plugin::groupSetRole;
my $remainingOptions = OVH::Bastion::Plugin::begin(
argv => \@ARGV,
header => "revoke an account as aclkeeper of a group",
options => {"account=s" => \my $account, "group=s" => \my $group},
helptext => <<'EOF',
Remove the group aclkeeper role from an account
Usage: --osh SCRIPT_NAME --group GROUP --account ACCOUNT
--group GROUP which group to remove ACCOUNT as an aclkeeper of
--account ACCOUNT which account to remove as an aclkeeper of GROUP
The specified account will no longer be able to manage the server list of this group
EOF
);
my $fnret = OVH::Bastion::Plugin::groupSetRole::act(
account => $account,
group => $group,
action => 'del',
type => 'aclkeeper',
sudo => 0,
silentoverride => 0,
self => $self,
scriptName => $scriptName,
savedArgs => $savedArgs
);
help() if not $fnret;
osh_exit($fnret);

View file

@ -0,0 +1,10 @@
{
"interactive": [
"groupDelAclkeeper" , {"ac" : ["--account"]},
"groupDelAclkeeper --account" , {"ac" : ["<ACCOUNT>"]},
"groupDelAclkeeper --account \\S+" , {"ac" : ["--group"]},
"groupDelAclkeeper --account \\S+ --group" , {"ac" : ["<GROUP>"]},
"groupDelAclkeeper --account \\S+ --group \\S+" , {"pr" : ["<enter>"]}
],
"master_only": true
}

View file

@ -0,0 +1,42 @@
#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;
use File::Basename;
use lib dirname(__FILE__) . '/../../../lib/perl';
use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT help );
use OVH::Bastion::Plugin::groupSetRole;
my ($account, $group);
my $remainingOptions = OVH::Bastion::Plugin::begin(
argv => \@ARGV,
header => "revoke an account as gatekeeper of a group",
options => {"account=s", \$account, "group=s", \$group},
helptext => <<'EOF',
Remove the group gatekeeper role from an account
Usage: --osh SCRIPT_NAME --group GROUP --account ACCOUNT
--group GROUP which group to remove ACCOUNT as a gatekeeper of
--account ACCOUNT which account to remove as a gatekeeper of GROUP
The specified account will no longer be able to manager the members nor
the guest list of this group
EOF
);
my $fnret = OVH::Bastion::Plugin::groupSetRole::act(
account => $account,
group => $group,
action => 'del',
type => 'gatekeeper',
sudo => 0,
silentoverride => 0,
self => $self,
scriptName => $scriptName,
savedArgs => $savedArgs
);
help() if not $fnret;
osh_exit($fnret);

View file

@ -0,0 +1,10 @@
{
"interactive": [
"groupDelGatekeeper" , {"ac" : ["--account"]},
"groupDelGatekeeper --account" , {"ac" : ["<ACCOUNT>"]},
"groupDelGatekeeper --account \\S+" , {"ac" : ["--group"]},
"groupDelGatekeeper --account \\S+ --group" , {"ac" : ["<GROUP>"]},
"groupDelGatekeeper --account \\S+ --group \\S+" , {"pr" : ["<enter>"]}
],
"master_only": true
}

View file

@ -0,0 +1,42 @@
#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;
use File::Basename;
use lib dirname(__FILE__) . '/../../../lib/perl';
use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT help );
use OVH::Bastion::Plugin::groupSetRole;
my ($account, $group);
my $remainingOptions = OVH::Bastion::Plugin::begin(
argv => \@ARGV,
header => "revoke an account as owner of a group",
options => {"account=s", \$account, "group=s", \$group},
helptext => <<'EOF',
Remove the group owner role from an account
Usage: --osh SCRIPT_NAME --group GROUP --account ACCOUNT
--group GROUP which group to set ACCOUNT as an owner of
--account ACCOUNT which account to set as an owner of GROUP
The specified account will no longer be able to manage the owner,
gatekeeper and aclkeeper lists of this group
EOF
);
my $fnret = OVH::Bastion::Plugin::groupSetRole::act(
account => $account,
group => $group,
action => 'del',
type => 'owner',
sudo => 0,
silentoverride => 0,
self => $self,
scriptName => $scriptName,
savedArgs => $savedArgs
);
help() if not $fnret;
osh_exit($fnret);

View file

@ -0,0 +1,10 @@
{
"interactive": [
"groupDelOwner" , {"ac" : ["--account"]},
"groupDelOwner --account" , {"ac" : ["<ACCOUNT>"]},
"groupDelOwner --account \\S+" , {"ac" : ["--group"]},
"groupDelOwner --account \\S+ --group" , {"ac" : ["<GROUP>"]},
"groupDelOwner --account \\S+ --group \\S+" , {"pr" : ["<enter>"]}
],
"master_only": true
}

View file

@ -0,0 +1,79 @@
#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;
use File::Basename;
use lib dirname(__FILE__) . '/../../../lib/perl';
use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT help );
use OVH::Bastion::Plugin::generatePassword;
my $remainingOptions = OVH::Bastion::Plugin::begin(
argv => \@ARGV,
header => "generating a new egress password for the group",
options => {
"size=i" => \my $size,
"group=s" => \my $group,
"do-it" => \my $doIt,
},
helptext => <<'EOF'
Generate a new egress password for the group
Usage: --osh SCRIPT_NAME --group GROUP [--size SIZE] --do-it
--group GROUP Specify which group you want to generate a password for
--size SIZE Specify the number of characters of the password to generate
--do-it Required for the password to actually be generated, BEWARE: please read the note below
Generate a new egress password to be used for ssh or telnet
NOTE: this is only needed for devices that don't support key-based SSH,
in most cases you should ignore this command completely, unless you
know that devices you need to access only support telnet or password-based SSH.
BEWARE: once a new password is generated this way, it'll be set as the new
egress password to use right away for the group, for any access that requires it.
A fallback mechanism exists that will auto-try the previous password if this one
doesn't work, but please ensure that this new password is deployed on the remote
devices as soon as possible.
EOF
);
# code
my $fnret;
$size = 16 if not defined $size;
if (not $group) {
help();
osh_exit 'ERR_MISSING_PARAMETER', "Expected a --group argument";
}
$fnret = OVH::Bastion::Plugin::generatePassword::preconditions(self => $self, context => 'group', group => $group, size => $size);
$fnret or osh_exit($fnret);
# get returned untainted value
$group = $fnret->value->{'group'};
my $shortGroup = $fnret->value->{'shortGroup'};
if (not $doIt) {
help();
osh_exit('ERR_MISSING_PARAMETER', "Missing mandatory parameter: please read the BEWARE note above.");
}
my @command = qw{ sudo -n -u };
push @command, $group;
push @command, qw{ -- /usr/bin/env perl -T };
push @command, $OVH::Bastion::BASEPATH . '/bin/helper/osh-groupGeneratePassword';
push @command, "--group", $group, "--size", $size;
$fnret = OVH::Bastion::helper(cmd => \@command);
$fnret or osh_exit($fnret);
osh_info "Generated a new password of length $size for group $shortGroup, hashes follow:";
osh_info "md5crypt: " . $fnret->value->{'hashes'}{'md5crypt'} . "\n";
osh_info "sha256crypt: " . $fnret->value->{'hashes'}{'sha256crypt'} . "\n";
osh_info "sha512crypt: " . $fnret->value->{'hashes'}{'sha512crypt'} . "\n";
osh_info "This new password will now be used by default.";
osh_exit $fnret;

View file

@ -0,0 +1,11 @@
{
"interactive": [
"groupGeneratePassword" , {"ac" : ["--group"]},
"groupGeneratePassword --group" , {"ac" : ["<GROUP>"]},
"groupGeneratePassword --group \\S+" , {"ac" : ["<enter>", "--size"]},
"groupGeneratePassword --group \\S+ --size" , {"pr" : ["<SIZE>"]},
"groupGeneratePassword --group \\S+ --size \\d+" , {"pr" : ["<enter>"]}
],
"master_only": true,
"terminal_mode": "noecho"
}

View file

@ -0,0 +1,65 @@
#! /usr/bin/env perl
# vim: set filetype=perl ts=4 sw=4 sts=4 et:
use common::sense;
use File::Basename;
use lib dirname(__FILE__) . '/../../../lib/perl';
use OVH::Result;
use OVH::Bastion;
use OVH::Bastion::Plugin qw( :DEFAULT help );
my $remainingOptions = OVH::Bastion::Plugin::begin(
argv => \@ARGV,
header => "modify the configuration of a group",
options => {
"group=s" => \my $group,
"mfa-required=s" => \my $mfaRequired,
"guest-ttl-limit=s" => \my $ttl,
},
helptext => <<'EOF',
Modify the configuration of a group
Usage: --osh SCRIPT_NAME --group GROUP [--mfa-required password|totp|any|none] [--guest-ttl-limit DURATION]
--group GROUP Name of the group to modify
--mfa-required password|totp|any|none Enforce UNIX password requirement, or TOTP requirement, or any MFA requirement, when connecting to a server of the group
--guest-ttl-limit DURATION This group will enforce TTL setting, on guest access creation, to be set, and not to a higher value than DURATION,
set to zero to allow guest accesses creation without any TTL set (default)
EOF
);
my $fnret;
if (!$group) {
help();
osh_exit 'ERR_MISSING_PARAMETER', "Missing mandatory parameter 'group'";
}
if (!$mfaRequired && !defined $ttl) {
help();
osh_exit 'ERR_MISSING_PARAMETER', "Nothing to modify";
}
if (defined $ttl) {
$fnret = OVH::Bastion::is_valid_ttl(ttl => $ttl);
$fnret or osh_exit $fnret;
$ttl = $fnret->value->{'seconds'};
}
$fnret = OVH::Bastion::is_valid_group_and_existing(group => $group, groupType => 'key');
$fnret or osh_exit $fnret;
$group = $fnret->value->{'group'};
my $shortGroup = $fnret->value->{'shortGroup'};
if (defined $mfaRequired && !grep { $mfaRequired eq $_ } qw{ password totp any none }) {
help();
osh_exit 'ERR_INVALID_PARAMETER', "Expected 'password', 'totp', 'any' or 'none' as parameter to --mfa-required";
}
my @command = qw{ sudo -n -u };
push @command, $group;
push @command, qw{ -- /usr/bin/env perl -T };
push @command, $OVH::Bastion::BASEPATH . '/bin/helper/osh-groupModify';
push @command, '--group', $group;
push @command, '--mfa-required', $mfaRequired if $mfaRequired;
push @command, '--guest-ttl-limit', $ttl if defined $ttl;
osh_exit OVH::Bastion::helper(cmd => \@command);

Some files were not shown because too many files have changed in this diff Show more