mirror of
https://github.com/ovh/the-bastion.git
synced 2024-12-25 09:03:56 +08:00
Initial commit
This commit is contained in:
commit
fde20136ef
463 changed files with 39401 additions and 0 deletions
4
.dockerignore
Normal file
4
.dockerignore
Normal file
|
@ -0,0 +1,4 @@
|
|||
.git
|
||||
doc
|
||||
docs
|
||||
*.tar.gz
|
32
.github/workflows/documentation.yml
vendored
Normal file
32
.github/workflows/documentation.yml
vendored
Normal 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
29
.github/workflows/tests.yml
vendored
Normal 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
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
doc/sphinx/_build
|
12
AUTHORS
Normal file
12
AUTHORS
Normal 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
83
CONTRIBUTING.md
Normal 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
15
CONTRIBUTORS
Normal 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
40
DESIGN.md
Normal 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
192
LICENSE
Normal 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
12
MAINTAINERS
Normal 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
149
README.md
Normal 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.
|
57
bin/admin/build-and-install-ttyrec.sh
Executable file
57
bin/admin/build-and-install-ttyrec.sh
Executable 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
715
bin/admin/check-consistency.pl
Executable 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
545
bin/admin/check-ssh-hardening.pl
Executable 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
128
bin/admin/fix-group-gid.sh
Executable 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
63
bin/admin/fixrights.sh
Executable 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 ""
|
60
bin/admin/grant-all-restricted-commands-to.sh
Executable file
60
bin/admin/grant-all-restricted-commands-to.sh
Executable 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
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
135
bin/admin/osh-sync-watcher.sh
Executable 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
146
bin/admin/packages-check.sh
Executable 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
110
bin/admin/rename-group.sh
Executable 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
61
bin/admin/restore-account.sh
Executable 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
205
bin/admin/setup-encryption.sh
Executable 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/
|
||||
|
25
bin/admin/setup-first-admin-account.sh
Executable file
25
bin/admin/setup-first-admin-account.sh
Executable 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
141
bin/admin/setup-gpg.sh
Executable 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
44
bin/admin/unlock-home.sh
Executable 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
125
bin/cron/osh-backup-acl-keys.sh
Executable 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
|
36
bin/cron/osh-compress-old-logs.sh
Executable file
36
bin/cron/osh-compress-old-logs.sh
Executable 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
509
bin/cron/osh-encrypt-rsync.pl
Executable 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();
|
47
bin/cron/osh-lingering-sessions-reaper.sh
Executable file
47
bin/cron/osh-lingering-sessions-reaper.sh
Executable 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"
|
52
bin/cron/osh-orphaned-homedir.sh
Executable file
52
bin/cron/osh-orphaned-homedir.sh
Executable 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
105
bin/cron/osh-piv-grace-reaper.pl
Executable 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
36
bin/cron/osh-rotate-ttyrec.sh
Executable 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
31
bin/dev/debug_toggle.sh
Executable 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
48
bin/dev/perl-check.sh
Executable 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
19
bin/dev/perl-critic.sh
Executable 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
48
bin/dev/perl-tidy.sh
Executable 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
56
bin/dev/perl-use-all.pl
Executable 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
44
bin/dev/perlcriticrc
Normal 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
42
bin/dev/shell-check.sh
Executable 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"
|
83
bin/helper/osh-accountAddGroupServer
Executable file
83
bin/helper/osh-accountAddGroupServer
Executable 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
436
bin/helper/osh-accountCreate
Executable 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
218
bin/helper/osh-accountDelete
Executable 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'});
|
56
bin/helper/osh-accountGeneratePassword
Executable file
56
bin/helper/osh-accountGeneratePassword
Executable 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));
|
91
bin/helper/osh-accountGetPasswordInfo
Executable file
91
bin/helper/osh-accountGetPasswordInfo
Executable 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);
|
74
bin/helper/osh-accountListEgressKeys
Executable file
74
bin/helper/osh-accountListEgressKeys
Executable 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));
|
87
bin/helper/osh-accountListIngressKeys
Executable file
87
bin/helper/osh-accountListIngressKeys
Executable 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);
|
74
bin/helper/osh-accountListPasswords
Executable file
74
bin/helper/osh-accountListPasswords
Executable 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));
|
94
bin/helper/osh-accountMFAResetPassword
Executable file
94
bin/helper/osh-accountMFAResetPassword
Executable 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');
|
91
bin/helper/osh-accountMFAResetTOTP
Executable file
91
bin/helper/osh-accountMFAResetTOTP
Executable 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
334
bin/helper/osh-accountModify
Executable 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);
|
141
bin/helper/osh-accountModifyCommand
Executable file
141
bin/helper/osh-accountModifyCommand
Executable 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);
|
106
bin/helper/osh-accountModifyPersonalAccess
Executable file
106
bin/helper/osh-accountModifyPersonalAccess
Executable 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
187
bin/helper/osh-accountPIV
Executable 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
95
bin/helper/osh-accountUnexpire
Executable 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
113
bin/helper/osh-adminMaintenance
Executable 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
114
bin/helper/osh-groupAddServer
Executable 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);
|
147
bin/helper/osh-groupAddSymlinkToAccount
Executable file
147
bin/helper/osh-groupAddSymlinkToAccount
Executable 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
287
bin/helper/osh-groupCreate
Executable 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
196
bin/helper/osh-groupDelete
Executable 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');
|
56
bin/helper/osh-groupGeneratePassword
Executable file
56
bin/helper/osh-groupGeneratePassword
Executable 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
128
bin/helper/osh-groupModify
Executable 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
140
bin/helper/osh-groupSetRole
Executable 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");
|
112
bin/helper/osh-selfMFASetupPassword
Executable file
112
bin/helper/osh-selfMFASetupPassword
Executable 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
71
bin/helper/osh-selfMFASetupTOTP
Executable 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');
|
32
bin/other/check-active-account-fortestsonly.pl
Executable file
32
bin/other/check-active-account-fortestsonly.pl
Executable 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;
|
54
bin/other/check-active-account-simple.pl
Executable file
54
bin/other/check-active-account-simple.pl
Executable 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;
|
46
bin/plugin/admin/adminMaintenance
Executable file
46
bin/plugin/admin/adminMaintenance
Executable 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);
|
9
bin/plugin/admin/adminMaintenance.json
Normal file
9
bin/plugin/admin/adminMaintenance.json
Normal 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
73
bin/plugin/admin/adminSudo
Executable 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;
|
8
bin/plugin/admin/adminSudo.json
Normal file
8
bin/plugin/admin/adminSudo.json
Normal 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>" ]}
|
||||
]
|
||||
}
|
144
bin/plugin/group-aclkeeper/groupAddServer
Executable file
144
bin/plugin/group-aclkeeper/groupAddServer
Executable 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);
|
14
bin/plugin/group-aclkeeper/groupAddServer.json
Normal file
14
bin/plugin/group-aclkeeper/groupAddServer.json
Normal 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
|
||||
}
|
100
bin/plugin/group-aclkeeper/groupDelServer
Executable file
100
bin/plugin/group-aclkeeper/groupDelServer
Executable 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);
|
14
bin/plugin/group-aclkeeper/groupDelServer.json
Normal file
14
bin/plugin/group-aclkeeper/groupDelServer.json
Normal 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
|
||||
}
|
93
bin/plugin/group-gatekeeper/groupAddGuestAccess
Executable file
93
bin/plugin/group-gatekeeper/groupAddGuestAccess
Executable 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);
|
17
bin/plugin/group-gatekeeper/groupAddGuestAccess.json
Normal file
17
bin/plugin/group-gatekeeper/groupAddGuestAccess.json
Normal 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
|
||||
}
|
46
bin/plugin/group-gatekeeper/groupAddMember
Executable file
46
bin/plugin/group-gatekeeper/groupAddMember
Executable 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);
|
10
bin/plugin/group-gatekeeper/groupAddMember.json
Normal file
10
bin/plugin/group-gatekeeper/groupAddMember.json
Normal 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
|
||||
}
|
84
bin/plugin/group-gatekeeper/groupDelGuestAccess
Executable file
84
bin/plugin/group-gatekeeper/groupDelGuestAccess
Executable 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);
|
17
bin/plugin/group-gatekeeper/groupDelGuestAccess.json
Normal file
17
bin/plugin/group-gatekeeper/groupDelGuestAccess.json
Normal 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
|
||||
}
|
46
bin/plugin/group-gatekeeper/groupDelMember
Executable file
46
bin/plugin/group-gatekeeper/groupDelMember
Executable 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);
|
10
bin/plugin/group-gatekeeper/groupDelMember.json
Normal file
10
bin/plugin/group-gatekeeper/groupDelMember.json
Normal 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
|
||||
}
|
52
bin/plugin/group-gatekeeper/groupListGuestAccesses
Executable file
52
bin/plugin/group-gatekeeper/groupListGuestAccesses
Executable 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);
|
9
bin/plugin/group-gatekeeper/groupListGuestAccesses.json
Normal file
9
bin/plugin/group-gatekeeper/groupListGuestAccesses.json
Normal 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>"]}
|
||||
]
|
||||
}
|
40
bin/plugin/group-owner/groupAddAclkeeper
Executable file
40
bin/plugin/group-owner/groupAddAclkeeper
Executable 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);
|
10
bin/plugin/group-owner/groupAddAclkeeper.json
Normal file
10
bin/plugin/group-owner/groupAddAclkeeper.json
Normal 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
|
||||
}
|
42
bin/plugin/group-owner/groupAddGatekeeper
Executable file
42
bin/plugin/group-owner/groupAddGatekeeper
Executable 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);
|
10
bin/plugin/group-owner/groupAddGatekeeper.json
Normal file
10
bin/plugin/group-owner/groupAddGatekeeper.json
Normal 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
|
||||
}
|
44
bin/plugin/group-owner/groupAddOwner
Executable file
44
bin/plugin/group-owner/groupAddOwner
Executable 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);
|
10
bin/plugin/group-owner/groupAddOwner.json
Normal file
10
bin/plugin/group-owner/groupAddOwner.json
Normal 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
|
||||
}
|
40
bin/plugin/group-owner/groupDelAclkeeper
Executable file
40
bin/plugin/group-owner/groupDelAclkeeper
Executable 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);
|
10
bin/plugin/group-owner/groupDelAclkeeper.json
Normal file
10
bin/plugin/group-owner/groupDelAclkeeper.json
Normal 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
|
||||
}
|
42
bin/plugin/group-owner/groupDelGatekeeper
Executable file
42
bin/plugin/group-owner/groupDelGatekeeper
Executable 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);
|
10
bin/plugin/group-owner/groupDelGatekeeper.json
Normal file
10
bin/plugin/group-owner/groupDelGatekeeper.json
Normal 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
|
||||
}
|
42
bin/plugin/group-owner/groupDelOwner
Executable file
42
bin/plugin/group-owner/groupDelOwner
Executable 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);
|
10
bin/plugin/group-owner/groupDelOwner.json
Normal file
10
bin/plugin/group-owner/groupDelOwner.json
Normal 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
|
||||
}
|
79
bin/plugin/group-owner/groupGeneratePassword
Executable file
79
bin/plugin/group-owner/groupGeneratePassword
Executable 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;
|
11
bin/plugin/group-owner/groupGeneratePassword.json
Normal file
11
bin/plugin/group-owner/groupGeneratePassword.json
Normal 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"
|
||||
}
|
65
bin/plugin/group-owner/groupModify
Executable file
65
bin/plugin/group-owner/groupModify
Executable 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
Loading…
Reference in a new issue