diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..6ace149 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,15 @@ +# Contributing Guidelines + +:confetti_ball::medal_military: First of all, thank you for contributing! :medal_military::confetti_ball: + +## Issue + +- Search for an already opened [issue](https://github.com/ReasonSoftware/ssh-manager/issues) before submitting a [new one](https://github.com/ReasonSoftware/ssh-manager/issues/new/choose). +- Provide as much information as you can. + +## Pull Request + +- Ensure [Pull Request](https://github.com/ReasonSoftware/ssh-manager/pulls) description clearly describes the problem and solution. +- Make sure all Tests are passed and there is no Code Coverage degradation. +- Add more tests wherever possible. +- Please follow [AngularJS Commit Message Guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-guidelines) diff --git a/.github/ISSUE_TEMPLATE/report_a_bug.md b/.github/ISSUE_TEMPLATE/report_a_bug.md new file mode 100644 index 0000000..17c4fdd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/report_a_bug.md @@ -0,0 +1,96 @@ +--- +name: Report a Bug +about: Create a report to help us improve +title: '' +labels: '' +assignees: anton-yurchenko + +--- + +### Description + +A clear and concise description of what the bug is + +### Version + +`Provide an Application Version` + +*First log message contains the version number...* + +### Log + +```Attach an execution log``` + +*In case your security policy does not allow you to provide usernames, you may replace them with something like `user-1`/`user-2`.* + +#### Sanitized Central Configuration + +:warning: **Obfuscate real information** :warning: + +
Clich Here to Expand + +```json +{ + "users": { + "user.1": "AAA", + "user.2": "BBB", + "user.3": "CCC", + "user.4": "DDD", + "user.5": "EEE", + "user.6": "FFF" + }, + "server_groups": { + "backend": { + "sudoers": [ + "user.2" + ], + "users": [ + "user.1", + "user.4", + "user.5" + ] + }, + "poc": { + "sudoers": [ + "user.1", + "user.2", + "user.4" + ], + "users": [ + "user.6" + ] + }, + "devops": { + "sudoers": [ + "user.2" + ], + "users": [ + "user.3", + "user.5" + ] + } + } +} +``` + +
+ +#### Sanitized Server Configuration + +:warning: **Obfuscate real information** :warning: + +
Clich Here to Expand + +```yaml +secret_name: XXX +groups: + - A + - B + - C +``` + +
+ +### Screenshots + +If applicable, add screenshots to help explain your problem. diff --git a/.github/ISSUE_TEMPLATE/suggest_a_change.md b/.github/ISSUE_TEMPLATE/suggest_a_change.md new file mode 100644 index 0000000..fc22388 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/suggest_a_change.md @@ -0,0 +1,17 @@ +--- +name: Suggest a Change +about: Suggest an enhancement to help us improve +title: '' +labels: '' +assignees: anton-yurchenko + +--- + +### Description +A clear and concise description of what an enhancement is about + +### Reference +If applicable, add external documentation links. + +### Screenshots +If applicable, add screenshots to help explain your problem. diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md new file mode 100644 index 0000000..63d03cd --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -0,0 +1,93 @@ +## Description + +Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. + +Fixes # (issue) + +## Type of change + +Please delete options that are not relevant. + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] This change requires a documentation update + +## How Has This Been Tested + +Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration + +- [ ] Test A +- [ ] Test B + +#### Test Central Configuration + +
Clich Here to Expand + +```json +{ + "users": { + "user.1": "ssh-rsa AAA...", + "user.2": "ssh-rsa AAA...", + "user.3": "ssh-rsa AAA...", + "user.4": "ssh-rsa AAA...", + "user.5": "ssh-rsa AAA...", + "user.6": "ssh-rsa AAA..." + }, + "server_groups": { + "backend": { + "sudoers": [ + "user.2" + ], + "users": [ + "user.1", + "user.4", + "user.5" + ] + }, + "poc": { + "sudoers": [ + "user.1", + "user.2", + "user.4" + ], + "users": [ + "user.6" + ] + }, + "devops": { + "sudoers": [ + "user.2" + ], + "users": [ + "user.3", + "user.5" + ] + } + } +} +``` + +
+ +#### Test Server Configuration + +
Clich Here to Expand + +```yaml +secret_name: ssh-manager +groups: + - devops +``` + +
+ +# Checklist + +- [ ] My code follows the style guidelines of this project +- [ ] I have performed a self-review of my own code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..3c30ba6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,18 @@ +version: 2 +updates: + - package-ecosystem: gomod + directory: "/" + schedule: + interval: monthly + assignees: + - anton-yurchenko + labels: + - dependencies + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: monthly + target-branch: master + assignees: + - anton-yurchenko diff --git a/.github/no-response.yml b/.github/no-response.yml new file mode 100644 index 0000000..7193eaa --- /dev/null +++ b/.github/no-response.yml @@ -0,0 +1,13 @@ +# Configuration for probot-no-response - https://github.com/probot/no-response + +# Number of days of inactivity before an Issue is closed for lack of response +daysUntilClose: 14 +# Label requiring a response +responseRequiredLabel: more-information-needed +# Comment to post when closing an Issue for lack of response. Set to `false` to disable +closeComment: > + This issue has been automatically closed because there has been no response + to our request for more information from the original author. With only the + information that is currently in the issue, we don't have enough information + to take action. Please reach out if you have or find the answers we need so + that we can investigate further. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..71f6eb2 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,48 @@ +name: release +on: + push: + tags: + - v[0-9]+.[0-9]+.[0-9]+ + +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Init + uses: actions/setup-go@v2 + with: + go-version: 1.15 + id: go + + - name: Checkout + uses: actions/checkout@v2 + + - name: Install Dependencies + run: | + go get -v -t -d ./... + + - name: Lint + run: | + export PATH=$PATH:$(go env GOPATH)/bin + curl -s https://api.github.com/repos/golangci/golangci-lint/releases/latest | grep browser_download_url | grep linux-amd64 | cut -d : -f 2,3 | tr -d \" | wget -i - + tar -xvf golangci-lint-*-linux-amd64.tar.gz --strip=1 --no-anchored golangci-lint + ./golangci-lint run ./... + + - name: Test + run: go test -v $(go list ./... | grep -v vendor | grep -v mocks) -race -coverprofile=coverage.txt -covermode=atomic + + - name: Build + run: GOOS=linux GOARCH=amd64 go build -o ssh-manager + + - name: Pack + run: zip ssh-manager.zip ssh-manager LICENSE.md + + - name: Release + uses: docker://antonyurchenko/git-release:latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CHANGELOG_FILE: "CHANGELOG.md" + ALLOW_TAG_PREFIX: "true" + with: + args: ssh-manager.zip diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f06d171 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# OS +**/.DS_Store + +# IDE +.vscode/ +**/__debug_bin +.idea/ + +# project +coverage.txt +/vendor/ +/release/ +/docs/schema.json +/scripts/local.sh +/ssh-manager +/ssh-manager.zip +var/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..48ef21a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,2 @@ +## [1.0.0](https://github.com/ReasonSoftware/ssh-manager/releases/tag/v1.0.0) - 2021-02-10 +- First release \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..ab7afcf --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,201 @@ + 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 + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright © 2021 Reason Cybersecurity Ltd. + + 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. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..10fba0c --- /dev/null +++ b/Makefile @@ -0,0 +1,51 @@ +# global +BINARY := $(notdir $(CURDIR)) +GO_BIN_DIR := $(GOPATH)/bin +OSES := linux darwin windows +ARCHS := amd64 + +# unit tests +test: lint + @echo "unit testing..." + @go test $$(go list ./... | grep -v vendor | grep -v mocks) -race -coverprofile=coverage.txt -covermode=atomic + +# lint +.PHONY: lint +lint: $(GO_LINTER) + @echo "vendoring..." + @go mod vendor + @go mod tidy + @echo "linting..." + @golangci-lint run ./... + +# initialize +.PHONY: init +init: + @rm -f go.mod + @rm -f go.sum + @rm -rf ./vendor + @go mod init $$(pwd | awk -F'/' '{print "github.com/"$$(NF-1)"/"$$NF}') + +# linter +GO_LINTER := $(GO_BIN_DIR)/golangci-lint +$(GO_LINTER): + @echo "installing linter..." + go get -u github.com/golangci/golangci-lint/cmd/golangci-lint + +.PHONY: release +release: test + @rm -rf ./release + @mkdir -p release + @for ARCH in $(ARCHS); do \ + for OS in $(OSES); do \ + if test "$$OS" = "windows"; then \ + GOOS=$$OS GOARCH=$$ARCH go build -o release/$(BINARY)-$$OS-$$ARCH.exe; \ + else \ + GOOS=$$OS GOARCH=$$ARCH go build -o release/$(BINARY)-$$OS-$$ARCH; \ + fi; \ + done; \ + done + +.PHONY: codecov +codecov: test + @go tool cover -html=coverage.txt diff --git a/README.md b/README.md new file mode 100644 index 0000000..1be2e49 --- /dev/null +++ b/README.md @@ -0,0 +1,216 @@ +# ssh-manager + +[![Release](https://img.shields.io/github/v/release/ReasonSoftware/ssh-manager)](https://github.com/ReasonSoftware/ssh-manager/releases/latest) +[![Release](https://github.com/ReasonSoftware/ssh-manager/workflows/release/badge.svg)](https://github.com/ReasonSoftware/ssh-manager/actions) +[![Go Report Card](https://goreportcard.com/badge/github.com/ReasonSoftware/ssh-manager)](https://goreportcard.com/report/github.com/ReasonSoftware/ssh-manager) +[![License](https://img.shields.io/github/license/ReasonSoftware/ssh-manager)](LICENSE.md) + +:closed_lock_with_key: Central **SSH Management Service** for **AWS Linux EC2** :vertical_traffic_light: + +![PIC](docs/pics/design.png) + +## Features + +- Automatically allow/deny SSH access to servers +- Easily manage `sudo` access +- Centrally manage team's SHS Keys +- Only public SSH key is used, private key never leave user's workstation +- Leverage AWS IAM for service authentication +- SystemD Service + +## Manual + +- Prepare [Central Configuration](#central-configuration) once +- Add new servers by: + - Complete [Server Configuration](#server-configuration) + - [Install](#installation) the service + +*It is strongly recommended to update the service once in a while* + +### Central Configuration + +1. Create configuration on **AWS Secret** which will hold a public ssh keys of your team members and server groups with a permissions mapping. + +
:information_source: AWS Secret Structure + +```json +{ + "users": { + "user.1": "ssh-rsa AAA...", + "user.2": "ssh-rsa AAA...", + "user.3": "ssh-rsa AAA...", + "user.4": "ssh-rsa AAA...", + "user.5": "ssh-rsa AAA...", + "user.6": "ssh-rsa AAA..." + }, + "server_groups": { + "backend": { + "sudoers": [ + "user.2" + ], + "users": [ + "user.1", + "user.4", + "user.5" + ] + }, + "poc": { + "sudoers": [ + "user.1", + "user.2", + "user.4" + ], + "users": [ + "user.6" + ] + }, + "devops": { + "sudoers": [ + "user.2" + ], + "users": [ + "user.3", + "user.5" + ] + } + } +} +``` + +
+ +2. Create IAM Policy to allow servers to fetch the secret. + +
:information_source: AWS IAM Policy + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "secretsmanager:GetSecretValue", + "Resource": "arn:aws:secretsmanager:*:*:secret:" + } + ] +} +``` + +
+ +### Server Configuration + +1. Create a local configuration file `/root/ssh-manager.yml` + +```yaml +secret_name: ssh-manager +region: us-west-1 +groups: + - devops + - poc +``` + +- `secret_name` (required) - AWS Secret name with a central configuration +- `region` - AWS region where a Secret is stored. Default **us-east-1** +- `groups` (required) - a list of server group names from a central configuration + +2. Create and attach an IAM Roles or configure an IAM User to allow EC2's to fetch the secret. + - If using User Authentication, configure the credentials for root user. + +### Installation + +- Download installation script: `curl https://raw.githubusercontent.com/ReasonSoftware/ssh-manager/main/scripts/install.sh --output install.sh` +- Execute with elevated privileges: `sudo bash install.sh` + +
:information_source: Manual Installation + +- Create an application directory: `mkdir -p /var/lib/ssh-manager` +- Download latest [release](https://github.com/ReasonSoftware/ssh-manager/releases/latest) unzip to `/var/lib/ssh-manager` +- Create **systemd** service under `/etc/systemd/system/ssh-manager.service` with the following content: + +``` +[Unit] +Description=Central SSH Management Service for AWS Linux EC2 +Wants=network-online.target +After=network-online.target + +[Service] +Type=oneshot +ExecStart=/var/lib/ssh-manager/ssh-manager +StandardOutput=journal +User=root + +[Install] +WantedBy=multi-user.target +``` + +- Create **systemd** timer under `/etc/systemd/system/ssh-manager.timer` with the following content: + +``` +[Unit] +Description=Timer for Central SSH Management Service +Wants=network-online.target +After=network-online.target + +[Timer] +Unit=ssh-manager.service +OnBootSec=10min +OnUnitInactiveSec=60min +Persistent=true + +[Install] +WantedBy=multi-user.target +``` + +- Reload **systemd** configuration: `systemctl daemon-reload` +- Enable **ssh-manager** service: `systemctl enable ssh-manager.service` +- Enable and start **ssh-manager** timer: `systemctl enable --now ssh-manager.timer` + +
+ +
:information_source: Update + +- Download latest [release](https://github.com/ReasonSoftware/ssh-manager/releases/latest) and replace `/var/lib/ssh-manager/ssh-manager` file + +
+ +
:information_source: Uninstall + +Decide what are you going to do with the users and either delete them (`userdel -r `) or change their primary group to some other group (`usermod -G `) + +- Delete systemd service and timer: + +```shell +systemctl stop ssh-manager.service +systemctl stop ssh-manager.timer +rm -f /etc/systemd/system/ssh-manager.* +``` + +- Delete application groups: + +```shell +groupdel ssh-manager-users +groupdel ssh-manager-sudoers +``` + +- Remove `%ssh-manager-sudoers ALL=(ALL) NOPASSWD: ALL` entry from `/etc/sudoers` file +- Delete app directory `rm -rf /var/lib/ssh-manager` +- Delete local configuration file `rm -f /root/ssh-manager.yml` + +
+ +## Examples + +- [Logs](docs/LOGS.md) + +## Notes + +- This service strongly relies on Linux capabilities to manage users and group, and will require the following to operate: `sudo`/`useradd`/`userdel`/`usermod`/`bash` +- Users default shell will be set to `bash` +- Assuming sudoers file is `/etc/sudoers` +- Application directory `/var/lib/ssh-manager` will be created automatically +- Custom linux groups `ssh-manager-users`/`ssh-manager-sudoers` will be created with a GID's `32109`/`32108` + +## License + +[Apache-2.0](LICENSE.md) © 2021 Reason Cybersecurity Ltd. diff --git a/docs/LOGS.md b/docs/LOGS.md new file mode 100644 index 0000000..ddf1c66 --- /dev/null +++ b/docs/LOGS.md @@ -0,0 +1,49 @@ +# Logs + +## First Execution + +
Click Here to Expand + +![PIC](pics/log-initial.png) + +
+ +## No Changes Required + +
Click Here to Expand + +![PIC](pics/log-no-changes.png) + +
+ +## User Public Key Updated + +
Click Here to Expand + +![PIC](pics/log-user-key-updated.png) + +
+ +## User Promoted + +
Click Here to Expand + +![PIC](pics/log-user-promoted.png) + +
+ +## User Demoted + +
Click Here to Expand + +![PIC](pics/log-user-demoted.png) + +
+ +## User Removed + +
Click Here to Expand + +![PIC](pics/log-user-removed.png) + +
diff --git a/docs/design.drawio b/docs/design.drawio new file mode 100644 index 0000000..fac64e1 --- /dev/null +++ b/docs/design.drawio @@ -0,0 +1 @@ +7Zxbb6M4GIZ/TS43MgeH9LJNO4eddtvdjtTLkQMO8ZRgZJzT/Pq1wSZgk5muNpUsjatKgdfGhu97HwzEZBItNoePDFXrB5rhYhKC7DCJbidhGEIAxIdUjq0SxHDeKjkjmdJOwjP5gZWoNsy3JMP1oCKntOCkGoopLUuc8oGGGKP7YbUVLYa9VijHlvCcosJWX0jG1606h+Ckf8IkX+ueA33EG6QrK6Feo4zue1J0N4kWjFLeLm0OC1zI6Om4tNt9OFPa7RjDJX/LBtmXcnd8fHl4zV6+JMs/079xsPlDtbJDxVYdsNpZftQRqCgpeRNFeCP+wRQEcBIuus8JFLUWTcnV/Fcl6rNXoluzS8JWGIpjWmKLQdNmCEfEMS0x+26EYKRvUwtHxNEmR/oGxk6K/+hmhxknwn/3aImLJ1oTTmgpor+knNONqLDmm0KsB7261wXJZR1OK6GiumpJWJEDzqSgilPhFMyE0KQUs7sdbjMr2xL2rGTGN4dcojxNSZ3S4GpaY7ZrNlqRoljQgrLGFhEAECSR3JAz+op1SUlL0cqNbU7tNLHL+NCTlFk/YrrBnB1FFVU6V9yoM0cUhTJ8UtmfQAyTqTjH9P7itsq6x6RmFalTQd51daJFLChg/gM8oYfHw+MmPEHoPD2Rp8fT4yg9M+fpiT09nh436QmB8/RAT4+nx1F6oPP0zDw9nh436YncH3sST4+nx1F6YufpmXt6PD1u0mM8sI4D5+C58vB4eNyEx3xg7SA9+itpj4/Hxzl8Zu7j4+caeHwcxcd8ZO0iPn62gcfHVXyg+/j46QYeH1fxMe59oH4M5ww8fraBh8dVeGauwwM9PB4eN+Exvy51Dx4/18DD4yo8sevw2FMNlih9xWX2S4bGnW663HL4wLS2102fWx7vC4HtdtPplsv7xg1sv5tet3yuPU63vCCl8Ix+qQwIUdguI8IuhpN6dt+vCcfPFUplVPfCpdKVtOTqtbYg1Osq8GDE1mhfx9Oc0W3VdPk5beCyS7/R8lvF8IbU2PK4cD+8ni3mM4MKtcdnqFRrBV5x2aI4DFLm983abQTUro91kaF6LTlujucCYCXGLAQw+jRB3zT1UQqSd2PJnniQ4R2tao+SR8ldlMIo0ewc9Yg0RlMQd+qAp/DdeLLnIlQ09TB5mByGKTAu+IB9wdeNQAOMwHth1B3ET+6PdCZXBT5cy1fiRTTENaBavE0LVNckHTpGlOvX3ZsQi3VtlmA6S34WT5wN3qi3o9mLFhwJltYYLhAnOzxofCyAqocneao4JQuaE0rMHNR0y1KstjqlwWooNidFzoyGOGI55lZDTT67w/4fKX7DF3+/aYoNHuM4uUyKrYbeO8Vv+HLqN02xSfH8QhTDsLvguXyS2f0/n76zEn9dRT+eHz78df+wfBr5qY2FCDpDYsNZIUe2JRNLuVwS4/uK5FuGmkcr55xQUo7PDO49D8g7fjkwl9lje/Gg9Ayx10c5lnIZETAFsLODNSKOOOTsIAnNd2evxq435xcaJcXq6RdU2kydfogmuvsX \ No newline at end of file diff --git a/docs/pics/design.png b/docs/pics/design.png new file mode 100644 index 0000000..59e2e04 Binary files /dev/null and b/docs/pics/design.png differ diff --git a/docs/pics/log-initial.png b/docs/pics/log-initial.png new file mode 100644 index 0000000..1fcefdf Binary files /dev/null and b/docs/pics/log-initial.png differ diff --git a/docs/pics/log-no-changes.png b/docs/pics/log-no-changes.png new file mode 100644 index 0000000..cbeb69f Binary files /dev/null and b/docs/pics/log-no-changes.png differ diff --git a/docs/pics/log-user-demoted.png b/docs/pics/log-user-demoted.png new file mode 100644 index 0000000..77d5f4a Binary files /dev/null and b/docs/pics/log-user-demoted.png differ diff --git a/docs/pics/log-user-key-updated.png b/docs/pics/log-user-key-updated.png new file mode 100644 index 0000000..17e41fb Binary files /dev/null and b/docs/pics/log-user-key-updated.png differ diff --git a/docs/pics/log-user-promoted.png b/docs/pics/log-user-promoted.png new file mode 100644 index 0000000..936847d Binary files /dev/null and b/docs/pics/log-user-promoted.png differ diff --git a/docs/pics/log-user-removed.png b/docs/pics/log-user-removed.png new file mode 100644 index 0000000..d732a87 Binary files /dev/null and b/docs/pics/log-user-removed.png differ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9a8f394 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module github.com/ReasonSoftware/ssh-manager + +go 1.15 + +require ( + github.com/aws/aws-sdk-go v1.37.8 + github.com/pkg/errors v0.9.1 + github.com/sirupsen/logrus v1.7.0 + github.com/spf13/viper v1.7.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5262dd0 --- /dev/null +++ b/go.sum @@ -0,0 +1,318 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aws/aws-sdk-go v1.37.8 h1:9kywcbuz6vQuTf+FD+U7FshafrHzmqUCjgAEiLuIJ8U= +github.com/aws/aws-sdk-go v1.37.8/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= +github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/internal/app/config.go b/internal/app/config.go new file mode 100644 index 0000000..d19a87d --- /dev/null +++ b/internal/app/config.go @@ -0,0 +1,42 @@ +package app + +import ( + "encoding/json" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/secretsmanager" + "github.com/pkg/errors" +) + +// Config represents a remote configuration +type Config struct { + Users map[string]string `json:"users"` + ServerGroups map[string]Members `json:"server_groups"` +} + +// Members ia a single server group in a configuration +type Members struct { + Sudoers []string `json:"sudoers"` + Users []string `json:"users"` +} + +// GetConfig fetches an AWS Secret and returns an application configuration +func GetConfig(service *secretsmanager.SecretsManager, name string) (*Config, error) { + result, err := service.GetSecretValue(&secretsmanager.GetSecretValueInput{ + SecretId: aws.String(name), + }) + if err != nil { + return nil, err + } + + if result.SecretString == nil { + return nil, errors.New("empty or a binary secret") + } + + output := &Config{} + if err := json.Unmarshal([]byte(*result.SecretString), output); err != nil { + return nil, errors.Wrap(err, "parsing error") + } + + return output, nil +} diff --git a/internal/app/loops.go b/internal/app/loops.go new file mode 100644 index 0000000..0fd81b0 --- /dev/null +++ b/internal/app/loops.go @@ -0,0 +1,86 @@ +package app + +import ( + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +// UsersLoop is a main loop for standard users creation and sudoers demotion +func (s *State) UsersLoop(users map[string]string) { +USERS: + for user, key := range users { + for _, stateUser := range s.Users { + if user == stateUser { + result, err := validatePublicKey(user, key) + if err != nil { + log.Error(errors.Wrapf(err, "error validating a user %v public key", user)) + } + + if result { + log.Infof("updated user %v public key", user) + } + + continue USERS + } + } + + for _, stateSudoer := range s.Sudoers { + if user == stateSudoer { + log.Infof("demoting a user: %v", user) + + if err := DemoteUser(user); err != nil { + log.Error(errors.Wrapf(err, "error demoting a user %v", user)) + } + + continue USERS + } + } + + if err := CreateUsers(user, key, false); err != nil { + log.Error(errors.Wrapf(err, "error creating a user '%v'", user)) + } + } +} + +// SudoersLoop is a main loop for sudo users creation and standard users promotion +func (s *State) SudoersLoop(sudoers map[string]string, listOfUsers []string) { +SUDOERS: + for sudoer, key := range sudoers { + for _, user := range listOfUsers { + if sudoer == user { + log.Errorf("user %v promotion denied because of a privilege conflict", sudoer) + + continue SUDOERS + } + } + + for _, stateSudoer := range s.Sudoers { + if sudoer == stateSudoer { + result, err := validatePublicKey(sudoer, key) + if err != nil { + log.Error(errors.Wrapf(err, "error validating a user %v public key", sudoer)) + } + + if result { + log.Infof("updated user %v public key", sudoer) + } + + continue SUDOERS + } + } + + for _, stateUser := range s.Users { + if sudoer == stateUser { + log.Infof("promoting a user: %v", sudoer) + if err := PromoteUser(sudoer); err != nil { + log.Error(errors.Wrapf(err, "error promoting a user %v", sudoer)) + } + continue SUDOERS + } + } + + if err := CreateUsers(sudoer, key, true); err != nil { + log.Error(errors.Wrapf(err, "error creating a user '%v'", sudoer)) + } + } +} diff --git a/internal/app/members.go b/internal/app/members.go new file mode 100644 index 0000000..a72b0d5 --- /dev/null +++ b/internal/app/members.go @@ -0,0 +1,45 @@ +package app + +// GetSudoers returns a map of sudo users and their public ssh keys for a matching server groups +func (c *Config) GetSudoers(serverGroups []string) map[string]string { + return c.getUniqueUsers(serverGroups, true) +} + +// GetUsers returns a map of users and their public ssh keys for a matching server groups +func (c *Config) GetUsers(serverGroups []string) map[string]string { + return c.getUniqueUsers(serverGroups, false) +} + +func (c *Config) getUniqueUsers(serverGroups []string, sudoers bool) map[string]string { + users := make([]string, 0) + for _, group := range serverGroups { + if sudoers { + users = combineUnique(users, c.ServerGroups[group].Sudoers) + } else { + users = combineUnique(users, c.ServerGroups[group].Users) + } + } + + output := make(map[string]string) + for _, user := range users { + output[user] = c.Users[user] + } + + return output +} + +func combineUnique(a []string, b []string) []string { + check := make(map[string]int) + d := append(a, b...) + res := make([]string, 0) + + for _, val := range d { + check[val] = 1 + } + + for letter := range check { + res = append(res, letter) + } + + return res +} diff --git a/internal/app/os.go b/internal/app/os.go new file mode 100644 index 0000000..81f1202 --- /dev/null +++ b/internal/app/os.go @@ -0,0 +1,105 @@ +package app + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "os/exec" + "os/user" + "strings" + + "github.com/pkg/errors" +) + +const ( + // SudoersGroup represent a sudoers unix group name + SudoersGroup string = "ssh-manager-sudoers" + // UsersGroup represent a users unix group name + UsersGroup string = "ssh-manager-users" +) + +// ValidateSudoersPermissions ensures that sudoers file contains a custom sudoers group. +func ValidateSudoersPermissions() error { + f := "/etc/sudoers" + instruction := fmt.Sprintf("%%%v ALL=(ALL) NOPASSWD: ALL", SudoersGroup) + + _, err := os.Stat(f) + if os.IsNotExist(err) { + return errors.Wrap(err, "sudoers file does not exists") + } + + origContent, err := ioutil.ReadFile(f) + if err != nil { + return errors.Wrap(err, "error reading sudoers file") + } + + lines := strings.Split(string(origContent), "\n") + + newContent := make([]string, 0) + for _, line := range lines { + if line == instruction { + return nil + } + + newContent = append(newContent, line) + } + + newContent = append(newContent, instruction) + + output := strings.Join(newContent, "\n") + err = ioutil.WriteFile(f, []byte(output), 0440) + if err != nil { + return errors.Wrap(err, "error writing to sudoers file") + } + + return nil +} + +// ValidateUsersGroup ensures that custom users group exists +func ValidateUsersGroup() error { + if err := createGroup(UsersGroup, 32108); err != nil { + return errors.Wrapf(err, "error validating group %v", UsersGroup) + } + + return nil +} + +// ValidateSudoersGroup ensures that custom sudoers group exists +func ValidateSudoersGroup() error { + if err := createGroup(SudoersGroup, 32109); err != nil { + return errors.Wrapf(err, "error validating group %v", SudoersGroup) + } + + return nil +} + +func createGroup(name string, id int64) error { + _, err := user.LookupGroup(name) + if err != nil { + _, unknown := err.(user.UnknownGroupError) + + if unknown { + if err := execShellCommand(fmt.Sprintf("groupadd -g %v %v", id, name)); err != nil { + return errors.Wrapf(err, "error creating %v group", name) + } + } else { + return errors.Wrapf(err, "error look up of a group %v", name) + } + } + + return nil +} + +func execShellCommand(command string) error { + cmd := exec.Command(strings.Split(command, " ")[0], strings.Split(command, " ")[1:len(strings.Split(command, " "))]...) + + var out bytes.Buffer + cmd.Stderr = &out + err := cmd.Run() + if err != nil { + return errors.Wrap(err, strings.ReplaceAll(out.String(), "\n", ";")) + } + + return nil +} diff --git a/internal/app/state.go b/internal/app/state.go new file mode 100644 index 0000000..b792fa5 --- /dev/null +++ b/internal/app/state.go @@ -0,0 +1,65 @@ +package app + +import ( + "encoding/json" + "io/ioutil" + "os" + + "github.com/pkg/errors" +) + +// State represents local application state and reflects a current status +// and a results of a previous run. +type State struct { + Users []string `json:"users"` + Sudoers []string `json:"sudoers"` +} + +// Update runtime state. +// +// **Warning**: This will not save the state to disk. +func (s *State) Update(users, sudoers []string) { + s.Users = users + s.Sudoers = sudoers +} + +// Save runtime state to disk +func (s *State) Save(file string) error { + stateFile, err := os.Open(file) + if err != nil { + return errors.Wrap(err, "error opening state file") + } + defer stateFile.Close() + + content, err := json.Marshal(s) + if err != nil { + return errors.Wrap(err, "error marshaling state to json") + } + + if err = ioutil.WriteFile(file, content, 0644); err != nil { + return errors.Wrap(err, "error writing state file") + } + + return nil +} + +// LoadState from disk +func LoadState(file string) (*State, error) { + stateFile, err := os.Open(file) + if err != nil { + return nil, errors.Wrap(err, "error opening state file") + } + defer stateFile.Close() + + content, err := ioutil.ReadAll(stateFile) + if err != nil { + return nil, errors.Wrap(err, "error reading state file") + } + + o := new(State) + if err = json.Unmarshal(content, o); err != nil { + return nil, errors.Wrap(err, "error unmarshaling state file") + } + + return o, nil +} diff --git a/internal/app/users.go b/internal/app/users.go new file mode 100644 index 0000000..f6553aa --- /dev/null +++ b/internal/app/users.go @@ -0,0 +1,191 @@ +package app + +import ( + "fmt" + "io/ioutil" + "math/rand" + "os" + "os/user" + + "path" + "strconv" + + "time" + + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +// CreateUsers will create a local users and assign them to the relevant +// groups. +func CreateUsers(user, key string, sudoer bool) error { + if sudoer { + log.Infof("adding a sudoer: %v", user) + } else { + log.Infof("adding a user: %v", user) + } + + // create user + command := fmt.Sprintf("useradd --create-home --home-dir /home/%v --shell /bin/bash --password %v %v", user, genPassword(), user) + if err := execShellCommand(command); err != nil { + return err + } + + // add ssh key + if err := updateAuthorizedKeys(user, key); err != nil { + return errors.Wrap(err, "error creating authorized_keys file") + } + + // promote user to sudoer + if sudoer { + if err := PromoteUser(user); err != nil { + return errors.Wrap(err, "error promoting a user") + } + } else { + if err := DemoteUser(user); err != nil { + return errors.Wrap(err, "error demoting a user") + } + } + + return nil +} + +func genPassword() string { + charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + + seededRand := rand.New( + rand.NewSource(time.Now().UnixNano())) + + b := make([]byte, 32) + for i := range b { + b[i] = charset[seededRand.Intn(len(charset))] + } + + return string(b) +} + +func updateAuthorizedKeys(username, key string) error { + // get uid/gui + u, err := user.Lookup(username) + if err != nil { + return errors.Wrap(err, "error identifying a user") + } + + uid, err := strconv.ParseInt(u.Uid, 10, 32) + if err != nil { + return errors.Wrap(err, "error identifying a user") + } + + g, err := user.LookupGroup(username) + if err != nil { + return errors.Wrap(err, "error identifying user's group") + } + + gid, err := strconv.ParseInt(g.Gid, 10, 32) + if err != nil { + return errors.Wrap(err, "error identifying user's group") + } + + dir := path.Join("/home", username, ".ssh") + file := path.Join(dir, "authorized_keys") + + // create user's .ssh directory + _, err = os.Stat(dir) + if os.IsNotExist(err) { + if err := os.MkdirAll(dir, 0700); err != nil { + return errors.Wrap(err, "error creating .ssh directory") + } + + if err := os.Chown(dir, int(uid), int(gid)); err != nil { + return errors.Wrap(err, "error updating .ssh ownership") + } + } else if err != nil { + return errors.Wrap(err, "error validating .ssh directory") + } + + // create user's authorized_keys file + if err := ioutil.WriteFile(file, []byte(key), 0600); err != nil { + return errors.Wrap(err, "error writing a file") + } + + if err := os.Chown(file, int(uid), int(gid)); err != nil { + return errors.Wrap(err, "error updating authorized_keys ownership") + } + + return nil +} + +func validatePublicKey(username, key string) (bool, error) { + f := path.Join("/home", username, ".ssh/authorized_keys") + + file, err := os.Open(f) + if err != nil { + return false, errors.Wrap(err, "error opening authorized_keys file") + } + defer file.Close() + + content, err := ioutil.ReadAll(file) + if err != nil { + return false, errors.Wrap(err, "error reading authorized_keys file") + } + + if string(content) != key { + if err := updateAuthorizedKeys(username, key); err != nil { + return false, errors.Wrap(err, "error updating authorized_keys file") + } + + return true, nil + } + + return false, nil +} + +// DeleteUsers that exists in a runtime state but not in a provided slices, +// which provided from a remote configuration. +func (s *State) DeleteUsers(users, sudoers []string) { + previousUsers := append(s.Users, s.Sudoers...) + currentUsers := append(users, sudoers...) + candidates := make([]string, 0) + + for _, user := range previousUsers { + removed := true + for _, u := range currentUsers { + if user == u { + removed = false + } + } + + if removed { + candidates = append(candidates, user) + } + } + + for _, user := range candidates { + log.Warnf("removing user: %v", user) + + command := fmt.Sprintf("userdel -r %v", user) + if err := execShellCommand(command); err != nil { + log.Errorf(errors.Wrap(err, "error deleting a user").Error()) + } + } +} + +// PromoteUser make standard user a sudo user +func PromoteUser(user string) error { + command := fmt.Sprintf("usermod -G %v %v", SudoersGroup, user) + if err := execShellCommand(command); err != nil { + return err + } + + return nil +} + +// DemoteUser make sudo user a standard user +func DemoteUser(user string) error { + command := fmt.Sprintf("usermod -G %v %v", UsersGroup, user) + if err := execShellCommand(command); err != nil { + return err + } + + return nil +} diff --git a/internal/app/utils.go b/internal/app/utils.go new file mode 100644 index 0000000..051c792 --- /dev/null +++ b/internal/app/utils.go @@ -0,0 +1,20 @@ +package app + +// SliceToString is a helper function to format a slice of strings +// into a comma separated string. +func SliceToString(slice []string) string { + var o string + for i, item := range slice { + if i == 0 { + o = item + } else { + o = o + ", " + item + } + } + + if o == "" { + return "none" + } + + return o +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..a924ac9 --- /dev/null +++ b/main.go @@ -0,0 +1,150 @@ +package main + +import ( + "io/ioutil" + "os" + "path" + + "github.com/ReasonSoftware/ssh-manager/internal/app" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/secretsmanager" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "github.com/spf13/viper" +) + +const ( + // Version of an application + Version string = "2.0.0" + // AppDir contains an home dir for an application files + AppDir string = "/var/lib/ssh-manager" + // StateFile contains a filename of a state file + StateFile string = "state.json" +) + +func init() { + // logger + log.SetReportCaller(false) + log.SetFormatter(&log.TextFormatter{ + ForceColors: false, + FullTimestamp: true, + DisableLevelTruncation: true, + DisableTimestamp: true, + }) + log.SetLevel(log.DebugLevel) + log.SetOutput(os.Stdout) + + // config + viper.SetConfigName("ssh-manager.yml") + viper.SetConfigType("yml") + viper.AddConfigPath("/root") + + if err := viper.ReadInConfig(); err != nil { + log.Fatal(errors.Wrap(err, "error reading configuration file")) + } + + if len(viper.GetStringSlice("groups")) == 0 { + log.Fatal("configuration does not contain any groups") + } + + if viper.GetString("secret_name") == "" { + log.Fatal("configuration does not contain an aws secret name") + } + + // state + _, err := os.Stat(AppDir) + if os.IsNotExist(err) { + if err := os.MkdirAll(AppDir, 0777); err != nil { + log.Fatal(errors.Wrap(err, "error creating application directory")) + } + } else if err != nil { + log.Fatal(errors.Wrap(err, "error validating application directory")) + } + + _, err = os.Stat(path.Join(AppDir, StateFile)) + if os.IsNotExist(err) { + if err = ioutil.WriteFile(path.Join(AppDir, StateFile), []byte("{}"), 0666); err != nil { + log.Fatal(errors.Wrap(err, "error creating state file")) + } + } else if err != nil { + log.Fatal(errors.Wrap(err, "error validating state file")) + } +} + +func main() { + log.Infof("ssh-manager v%v started", Version) + + // validate groups + log.Info("validating users group") + if err := app.ValidateUsersGroup(); err != nil { + log.Fatal(err) + } + + log.Info("validating sudoers group") + if err := app.ValidateSudoersGroup(); err != nil { + log.Fatal(err) + } + + log.Info("validating sudoers group permission") + if err := app.ValidateSudoersPermissions(); err != nil { + log.Fatal(err) + } + + state, err := app.LoadState(path.Join(AppDir, StateFile)) + if err != nil { + log.Fatal(errors.Wrap(err, "error loading state")) + } + log.Info("configured server groups: ", app.SliceToString(viper.GetStringSlice("groups"))) + + // get members + region := viper.GetString("region") + if region == "" { + region = "us-east-1" + } + + secretsManager := secretsmanager.New(session.Must(session.NewSession(&aws.Config{ + Region: ®ion, + }))) + + log.Info("fetching remote configuration") + conf, err := app.GetConfig(secretsManager, viper.GetString("secret_name")) + if err != nil { + log.Fatal(errors.Wrap(err, "error fetching remote configuration")) + } + + // warn about staled groups + for _, group := range viper.GetStringSlice("groups") { + if _, val := conf.ServerGroups[group]; !val { + log.Warnf("group %s does not exists on remote configuration", group) + } + } + + // get unique members + users := conf.GetUsers(viper.GetStringSlice("groups")) + listOfUsers := []string{} + for username := range users { + listOfUsers = append(listOfUsers, username) + } + log.Info("configured users: ", app.SliceToString(listOfUsers)) + + sudoers := conf.GetSudoers(viper.GetStringSlice("groups")) + listOfSudoers := []string{} + for username := range sudoers { + listOfSudoers = append(listOfSudoers, username) + } + log.Info("configured sudoers: ", app.SliceToString(listOfSudoers)) + + // configure users + state.UsersLoop(users) + state.SudoersLoop(sudoers, listOfUsers) + state.DeleteUsers(listOfUsers, listOfSudoers) + + // save state + state.Update(listOfUsers, listOfSudoers) + if err := state.Save(path.Join(AppDir, StateFile)); err != nil { + log.Fatal(errors.Wrap(err, "error saving the state")) + } + + log.Info("ssh-manager finished") +} diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100644 index 0000000..e80a30b --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +set -e + +rm -rf /var/lib/ssh-manager +mkdir -p /var/lib/ssh-manager + +wget $(curl -si https://api.github.com/repos/ReasonSoftware/ssh-manager/releases/latest | \ + grep browser_download_url | \ + awk -F': ' '{print $2}' | \ + tr -d '"') -O /var/lib/ssh-manager/ssh-manager.zip + +unzip -j /var/lib/ssh-manager/ssh-manager.zip -d /var/lib/ssh-manager +rm -f /var/lib/ssh-manager/ssh-manager.zip + +SERVICE=$(cat <<-EOF +[Unit] +Description=Central SSH Management Service for AWS Linux EC2 +Wants=network-online.target +After=network-online.target + +[Service] +Type=oneshot +ExecStart=/var/lib/ssh-manager/ssh-manager +StandardOutput=journal +User=root + +[Install] +WantedBy=multi-user.target +EOF +) + +echo "$SERVICE" > /etc/systemd/system/ssh-manager.service + +TIMER=$(cat <<-EOF +[Unit] +Description=Timer for Central SSH Management Service +Wants=network-online.target +After=network-online.target + +[Timer] +Unit=ssh-manager.service +OnBootSec=10min +OnUnitInactiveSec=60min +Persistent=true + +[Install] +WantedBy=multi-user.target +EOF +) + +echo "$TIMER" > /etc/systemd/system/ssh-manager.timer + +systemctl daemon-reload +systemctl enable ssh-manager.service +systemctl enable --now ssh-manager.timer \ No newline at end of file