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