├── examples ├── compose │ ├── .gitignore │ ├── .dockerignore │ ├── scripts │ │ ├── ssh-log-level │ │ ├── ssh-api-url │ │ └── ssh-authorized-keys-command │ ├── bastion.env.example │ ├── gak.env.example │ ├── docker-compose.yml │ └── README.md └── cloudformation │ ├── README.md │ └── cloudformation.yml ├── .github ├── ISSUE_TEMPLATE │ ├── question.md │ ├── config.yml │ ├── bug_report.md │ └── feature_request.md ├── banner.png ├── settings.yml ├── workflows │ ├── scheduled.yml │ ├── release-published.yml │ └── feature-branch.yml ├── PULL_REQUEST_TEMPLATE.md ├── auto-release.yml └── CODEOWNERS ├── rootfs ├── etc │ ├── mfa-failure.txt │ ├── profile.d │ │ ├── motd.sh │ │ └── hostname.sh │ ├── pam.d │ │ ├── google-authenticator │ │ ├── duo │ │ └── sshd │ ├── init.d │ │ ├── ssh-host-key │ │ ├── secure-proc │ │ ├── hostname │ │ ├── google-authenticator │ │ ├── ssh-authorized-keys-command │ │ ├── enforcer │ │ ├── slack │ │ ├── ssh-audit │ │ ├── duo │ │ └── rate-limit │ ├── fc.d │ │ └── 9.shell │ ├── motd │ ├── ssh │ │ ├── sshrc │ │ └── sshd_config │ ├── pam_duo.conf.env │ ├── enforce.d │ │ └── 2.clean-home │ ├── slack │ │ ├── pam-open_session-notification.json │ │ └── ssh-force-command-notification.json │ └── profile ├── init └── usr │ └── bin │ ├── github-authorized-keys │ ├── enforcer │ ├── fc │ ├── sudosh-add-user │ ├── setup-google-authenticator │ └── slack-notification ├── test ├── fixtures │ ├── auth │ │ ├── google_authenticator_code │ │ └── google_authenticator │ ├── sshrc │ │ └── sshrc_kill_test.sh │ ├── server_scripts │ │ ├── google_auth_qr_code_generator_test.sh │ │ ├── setup.sh │ │ └── google-auth.exp │ └── client_scripts │ │ ├── google_auth_test.sh │ │ └── sshrc_kill_test.sh ├── Dockerfile ├── docker-compose.yml └── test.sh ├── docs ├── demo.gif └── slack.png ├── .gitignore ├── .editorconfig ├── patches ├── openssh │ ├── cloudposse │ │ ├── obfuscate-version.diff │ │ └── original-command.diff │ └── alpine │ │ ├── default-internal-sftp.patch │ │ ├── avoid-redefined-warnings-when-building-with-utmps.patch │ │ ├── disable-forwarding-by-default.patch │ │ ├── include-config-dir.patch │ │ ├── fix-utmp.patch │ │ ├── sshd-session-flavor.patch │ │ └── disable-fzero-call-used-regs-used-on-ppc64le.patch └── README.md ├── Makefile ├── Dockerfile ├── README.yaml ├── LICENSE └── README.md /examples/compose/.gitignore: -------------------------------------------------------------------------------- 1 | *.env -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/compose/.dockerignore: -------------------------------------------------------------------------------- 1 | *.env -------------------------------------------------------------------------------- /rootfs/etc/mfa-failure.txt: -------------------------------------------------------------------------------- 1 | Access denied for %u@%H. 2 | -------------------------------------------------------------------------------- /test/fixtures/auth/google_authenticator_code: -------------------------------------------------------------------------------- 1 | 64744038 2 | -------------------------------------------------------------------------------- /rootfs/etc/profile.d/motd.sh: -------------------------------------------------------------------------------- 1 | [ -f /etc/motd ] && cat /etc/motd 2 | -------------------------------------------------------------------------------- /rootfs/etc/profile.d/hostname.sh: -------------------------------------------------------------------------------- 1 | export HOSTNAME="$(cat /etc/hostname)" 2 | -------------------------------------------------------------------------------- /docs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudposse/bastion/HEAD/docs/demo.gif -------------------------------------------------------------------------------- /docs/slack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudposse/bastion/HEAD/docs/slack.png -------------------------------------------------------------------------------- /.github/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudposse/bastion/HEAD/.github/banner.png -------------------------------------------------------------------------------- /test/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.21 2 | 3 | RUN apk add \ 4 | openssh-client \ 5 | sshpass 6 | -------------------------------------------------------------------------------- /rootfs/etc/pam.d/google-authenticator: -------------------------------------------------------------------------------- 1 | #%PAM-1.0 2 | auth requisite pam_google_authenticator.so nullok 3 | -------------------------------------------------------------------------------- /rootfs/etc/pam.d/duo: -------------------------------------------------------------------------------- 1 | #%PAM-1.0 2 | auth requisite /lib64/security/pam_duo.so conf=/etc/pam_duo.conf 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build-harness/ 2 | Makefile.build-harness 3 | 4 | .idea 5 | *.iml 6 | 7 | test/fixtures/auth/ida_rsa* 8 | -------------------------------------------------------------------------------- /test/fixtures/sshrc/sshrc_kill_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "$USER" == "sshrc_exit_test" ]; then 4 | exit 1 5 | fi 6 | -------------------------------------------------------------------------------- /test/fixtures/server_scripts/google_auth_qr_code_generator_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | 4 | cd /scripts 5 | 6 | expect google-auth.exp 7 | -------------------------------------------------------------------------------- /rootfs/etc/init.d/ssh-host-key: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ([ -f /etc/ssh/ssh_host_rsa_key ] || ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key -N "") 4 | 5 | 6 | -------------------------------------------------------------------------------- /rootfs/etc/fc.d/9.shell: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -n "${SSH_ORIGINAL_COMMAND}" ]; then 4 | exec ${SSH_ORIGINAL_COMMAND} 5 | else 6 | exec ${SHELL} -l 7 | fi 8 | -------------------------------------------------------------------------------- /examples/compose/scripts/ssh-log-level: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "- Setting SSH LogLevel to ${LOGLEVEL:-INFO}" 4 | echo "LogLevel ${LOGLEVEL:-INFO}" >> /etc/ssh/sshd_config -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Override for Makefile 2 | [{Makefile, makefile, GNUmakefile}] 3 | indent_style = tab 4 | indent_size = 4 5 | 6 | [Makefile.*] 7 | indent_style = tab 8 | indent_size = 4 9 | -------------------------------------------------------------------------------- /rootfs/etc/init.d/secure-proc: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "- Locking down /proc" 3 | chmod 700 /proc 4 | 5 | if [ "$?" == "1" ]; then 6 | echo "Do not have permissions to lockdown /proc" 7 | fi 8 | -------------------------------------------------------------------------------- /rootfs/etc/motd: -------------------------------------------------------------------------------- 1 | 2 | WARNING: Unauthorized access to this system is forbidden and will be 3 | prosecuted by law. By accessing this system, you agree that your actions 4 | may be monitored for any reason. 5 | 6 | -------------------------------------------------------------------------------- /rootfs/etc/init.d/hostname: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "${HOSTNAME}" > /etc/hostname 4 | 5 | # This won't work unless privileged (but we can try anyways) 6 | hostname -F /etc/hostname >/dev/null 2>&1 || true 7 | 8 | -------------------------------------------------------------------------------- /examples/cloudformation/README.md: -------------------------------------------------------------------------------- 1 | # CloudFormation Example 2 | 3 | This cloudformation example was contributed by [dmcd](https://github.com/dmcd). Please see [Issue #24](https://github.com/cloudposse/bastion/issues/24) for more details. 4 | -------------------------------------------------------------------------------- /test/fixtures/auth/google_authenticator: -------------------------------------------------------------------------------- 1 | RWNFRHHY2Z4H6ZKJNYJMXVBD7E 2 | " RATE_LIMIT 3 30 1623831923 1623831927 1623831929 3 | " WINDOW_SIZE 17 4 | " DISALLOW_REUSE 5 | " TOTP_AUTH 6 | 64744038 7 | 12220882 8 | 90440116 9 | 64503470 10 | 61850891 11 | -------------------------------------------------------------------------------- /examples/compose/scripts/ssh-api-url: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -n "${API_URL}" ] && [ "${API_URL}" != "none" ]; then 4 | echo "- Setting SSH Authorized Keys API URL" 5 | sed -i s!http://localhost:301/user/%s/authorized_keys!${API_URL}!g /usr/bin/github-authorized-keys 6 | fi -------------------------------------------------------------------------------- /test/fixtures/client_scripts/google_auth_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ping -c 1 -w 5 bastion > /dev/null 3 | 4 | # Add -vv for debugging. 5 | sshpass \ 6 | -P 'Verification code:' \ 7 | -f ./code \ 8 | ssh bastion@bastion \ 9 | -o StrictHostKeyChecking=no \ 10 | -- echo 'this is a test.' 11 | -------------------------------------------------------------------------------- /rootfs/etc/pam.d/sshd: -------------------------------------------------------------------------------- 1 | #%PAM-1.0 2 | auth include rate-limit 3 | auth required pam_env.so 4 | session optional pam_umask.so umask=0066 5 | auth include mfa 6 | account required pam_faillock.so 7 | session include sudosh 8 | session include enforcer 9 | -------------------------------------------------------------------------------- /test/fixtures/client_scripts/sshrc_kill_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ping -c 1 -w 5 bastion 3 | 4 | # Add -vv for debugging. 5 | sshpass \ 6 | -P 'Verification code:' \ 7 | -f ./code \ 8 | ssh sshrc_exit_test@bastion \ 9 | -o StrictHostKeyChecking=no \ 10 | -- echo 'this output should never print.' 11 | -------------------------------------------------------------------------------- /examples/compose/bastion.env.example: -------------------------------------------------------------------------------- 1 | API_URL=http://gak:301/user/%s/authorized_keys 2 | MFA_PROVIDER=google-authenticator 3 | SLACK_ENABLED=true 4 | SLACK_WEBHOOK_URL= 5 | SSH_AUTHORIZED_KEYS_COMMAND=/usr/bin/github-authorized-keys 6 | SSH_AUTHORIZED_KEYS_COMMAND_USER=root 7 | LOGLEVEL=DEBUG 8 | -------------------------------------------------------------------------------- /rootfs/etc/ssh/sshrc: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 4 | 5 | for script in /etc/ssh/sshrc.d/* ; do 6 | if [ -r $script ] ; then 7 | $script 8 | if [ $? -ne 0 ]; then 9 | echo "Goodbye" 10 | kill -TERM $PPID 11 | fi 12 | fi 13 | done 14 | -------------------------------------------------------------------------------- /rootfs/init: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | for script in /etc/init.d/* ; do 4 | if [ -r $script ] && [ -x $script ]; then 5 | echo "Initializing $(basename ${script})" 6 | $script 7 | if [ $? -ne 0 ]; then 8 | echo "FATAL: Failed to initialize" 9 | exit 1 10 | fi 11 | fi 12 | done 13 | 14 | exec /usr/sbin/sshd -D -e 15 | -------------------------------------------------------------------------------- /rootfs/etc/init.d/google-authenticator: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "${MFA_PROVIDER}" == "google-authenticator" ]; then 4 | echo "- Enabling Google Authenticator MFA" 5 | echo "#%PAM-1.0" > /etc/pam.d/mfa 6 | echo "auth include google-authenticator" >> /etc/pam.d/mfa 7 | ln -sf /usr/bin/setup-google-authenticator /etc/fc.d/0.setup-google-authenticator 8 | fi 9 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | # Upstream changes from _extends are only recognized when modifications are made to this file in the default branch. 2 | _extends: .github 3 | repository: 4 | name: bastion 5 | description: 🔒Secure Bastion implemented as Docker Container running Alpine Linux with Google Authenticator & DUO MFA support 6 | homepage: cloudposse.com/accelerate 7 | topics: "" 8 | -------------------------------------------------------------------------------- /rootfs/etc/init.d/ssh-authorized-keys-command: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -n "${SSH_AUTHORIZED_KEYS_COMMAND}" ] && [ "${SSH_AUTHORIZED_KEYS_COMMAND}" != "none" ]; then 4 | echo "- Enabling SSH Authorized Keys Command" 5 | echo "AuthorizedKeysCommand=${SSH_AUTHORIZED_KEYS_COMMAND}" >> /etc/ssh/sshd_config 6 | echo "AuthorizedKeysCommandUser=${SSH_AUTHORIZED_KEYS_COMMAND_USER}" >> /etc/ssh/sshd_config 7 | fi 8 | -------------------------------------------------------------------------------- /examples/compose/scripts/ssh-authorized-keys-command: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -n "${SSH_AUTHORIZED_KEYS_COMMAND}" ] && [ "${SSH_AUTHORIZED_KEYS_COMMAND}" != "none" ]; then 4 | echo "- Enabling SSH Authorized Keys Command" 5 | echo "AuthorizedKeysCommand ${SSH_AUTHORIZED_KEYS_COMMAND}" >> /etc/ssh/sshd_config 6 | echo "AuthorizedKeysCommandUser ${SSH_AUTHORIZED_KEYS_COMMAND_USER}" >> /etc/ssh/sshd_config 7 | fi -------------------------------------------------------------------------------- /.github/workflows/scheduled.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: scheduled 3 | on: 4 | workflow_dispatch: { } # Allows manually trigger this workflow 5 | schedule: 6 | - cron: "0 3 * * *" 7 | 8 | permissions: 9 | pull-requests: write 10 | id-token: write 11 | contents: write 12 | 13 | jobs: 14 | scheduled: 15 | uses: cloudposse/.github/.github/workflows/shared-terraform-scheduled.yml@main 16 | secrets: inherit 17 | -------------------------------------------------------------------------------- /patches/openssh/cloudposse/obfuscate-version.diff: -------------------------------------------------------------------------------- 1 | diff --git a/version.h b/version.h 2 | index 9bd910a64..6760b7b1b 100644 3 | --- a/version.h 4 | +++ b/version.h 5 | @@ -1,6 +1,6 @@ 6 | /* $OpenBSD: version.h,v 1.103 2024/09/19 22:17:44 djm Exp $ */ 7 | 8 | -#define SSH_VERSION "OpenSSH_9.9" 9 | +#define SSH_VERSION "SERVER" 10 | 11 | #define SSH_PORTABLE "p2" 12 | #define SSH_RELEASE SSH_VERSION SSH_PORTABLE 13 | -------------------------------------------------------------------------------- /rootfs/etc/init.d/enforcer: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | umask 0066 3 | 4 | if [ "${ENFORCER_ENABLED}" == "true" ]; then 5 | echo "- Enabling Enforcer" 6 | env | grep "^ENFORCER_" > /etc/enforcer 7 | echo "session required pam_exec.so stdout quiet /usr/bin/enforcer" > /etc/pam.d/enforcer 8 | else 9 | :>/etc/pam.d/enforcer 10 | fi 11 | 12 | if [ "${ENFORCER_CLEAN_HOME_ENABLED}" == "true" ]; then 13 | echo "- Enabling Clean Home" 14 | chmod 755 /etc/enforce.d/?.clean-home 15 | fi 16 | -------------------------------------------------------------------------------- /rootfs/etc/pam_duo.conf.env: -------------------------------------------------------------------------------- 1 | [duo] 2 | ; Duo integration key 3 | ikey = $DUO_IKEY 4 | ; Duo secret key 5 | skey = $DUO_SKEY 6 | ; Duo API hostname 7 | host = $DUO_HOST 8 | ; Duo failmode = secure means that if the module can't communicate with the service, authentication will be denied 9 | failmode = $DUO_FAILMODE 10 | ; Automatically send a push notification 11 | autopush = $DUO_AUTOPUSH 12 | ; Prompt only once (recommended setting when autopush is enabled) 13 | prompts = $DUO_PROMPTS 14 | -------------------------------------------------------------------------------- /rootfs/etc/enforce.d/2.clean-home: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z "${PAM_USER}" ]; then 4 | echo "ERROR: invalid execution" 5 | exit 1 6 | fi 7 | 8 | HOME=$(getent passwd ${PAM_USER} | cut -d: -f6) 9 | 10 | # Delete all dot files in user's home directory to prevent them from influencing the session 11 | if [ -d "${HOME}" ]; then 12 | find "${HOME}" -maxdepth 1 -type f -name '.*' '!' -name '.google_authenticator' | xargs --no-run-if-empty rm -f 13 | rm -f "${HOME}/.ssh/rc" 14 | fi 15 | -------------------------------------------------------------------------------- /patches/openssh/alpine/default-internal-sftp.patch: -------------------------------------------------------------------------------- 1 | set the default sftp to internal. 2 | this is better than the extra one, because it requires no extra support files 3 | with ChrootDirectory, and it does not fork so it is faster. 4 | --- a/sshd_config 5 | +++ b/sshd_config 6 | @@ -107,7 +107,7 @@ 7 | #Banner none 8 | 9 | # override default of no subsystems 10 | -Subsystem sftp /usr/libexec/sftp-server 11 | +Subsystem sftp internal-sftp 12 | 13 | # Example of overriding settings on a per-user basis 14 | #Match User anoncvs 15 | -------------------------------------------------------------------------------- /patches/openssh/alpine/avoid-redefined-warnings-when-building-with-utmps.patch: -------------------------------------------------------------------------------- 1 | From: Jakub Jirutka 2 | Date: Wed, 15 Dec 2021 22:37:42 +0100 3 | Subject: [PATCH] Avoid redefined warnings when building with utmps 4 | 5 | --- a/includes.h 6 | +++ b/includes.h 7 | @@ -62,6 +62,9 @@ 8 | #endif 9 | 10 | #ifdef HAVE_UTMP_H 11 | +/* _PATH_UTMP and _PATH_WTMP are defined both in paths.h and utmps/utmp.h. */ 12 | +# undef _PATH_UTMP 13 | +# undef _PATH_WTMP 14 | # include 15 | #endif 16 | #ifdef HAVE_UTMPX_H 17 | -------------------------------------------------------------------------------- /patches/openssh/alpine/disable-forwarding-by-default.patch: -------------------------------------------------------------------------------- 1 | --- openssh-7.7p1/sshd_config.old 2018-04-02 00:38:28.000000000 -0500 2 | +++ openssh-7.7p1/sshd_config 2018-07-29 03:08:16.340000000 -0500 3 | @@ -82,9 +82,10 @@ 4 | #UsePAM no 5 | 6 | #AllowAgentForwarding yes 7 | -#AllowTcpForwarding yes 8 | -#GatewayPorts no 9 | -#X11Forwarding no 10 | +# Feel free to re-enable these if your use case requires them. 11 | +AllowTcpForwarding no 12 | +GatewayPorts no 13 | +X11Forwarding no 14 | #X11DisplayOffset 10 15 | #X11UseLocalhost yes 16 | #PermitTTY yes 17 | -------------------------------------------------------------------------------- /rootfs/etc/init.d/slack: -------------------------------------------------------------------------------- 1 | if [ "${SLACK_ENABLED}" == "true" ]; then 2 | echo "- Enabling Slack Notifications" 3 | env | grep "^SLACK_" > /etc/slack/env 4 | if [ "${SLACK_HOOK}" == "sshrc" ]; then 5 | mkdir -p /etc/ssh/sshrc.d/ 6 | ln -sf /usr/bin/slack-notification /etc/ssh/sshrc.d/slack-notification 7 | chmod 644 /etc/slack/env 8 | elif [ "${SLACK_HOOK}" == "pam" ]; then 9 | ln -sf /usr/bin/slack-notification /etc/enforce.d/1.slack-notification 10 | chmod 600 /etc/slack/env 11 | else 12 | echo "Invalid SLACK_HOOK" >&2 13 | exit 1 14 | fi 15 | fi 16 | 17 | 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | 3 | contact_links: 4 | 5 | - name: Community Slack Team 6 | url: https://cloudposse.com/slack/ 7 | about: |- 8 | Please ask and answer questions here. 9 | 10 | - name: Office Hours 11 | url: https://cloudposse.com/office-hours/ 12 | about: |- 13 | Join us every Wednesday for FREE Office Hours (lunch & learn). 14 | 15 | - name: DevOps Accelerator Program 16 | url: https://cloudposse.com/accelerate/ 17 | about: |- 18 | Own your infrastructure in record time. We build it. You drive it. 19 | -------------------------------------------------------------------------------- /rootfs/etc/init.d/ssh-audit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "${SSH_AUDIT_ENABLED}" == "true" ]; then 4 | echo "- Enabling SSH Audit Logs" 5 | cat <<-EOF >/etc/sudoers.d/sudosh 6 | Defaults log_output 7 | Defaults!/usr/bin/sudoreplay !log_output 8 | Defaults!REBOOT !log_output 9 | EOF 10 | echo "# valid login shells" > /etc/shells 11 | echo "/usr/bin/sudosh" >> /etc/shells 12 | echo "session requisite pam_exec.so quiet /usr/bin/sudosh-add-user" > /etc/pam.d/sudosh 13 | [ -w /etc/passwd ] && usermod -s /usr/bin/sudosh root 14 | else 15 | echo "- Disabling SSH Audit Logs" 16 | :>/etc/pam.d/sudosh 17 | fi 18 | 19 | 20 | -------------------------------------------------------------------------------- /rootfs/etc/init.d/duo: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "${MFA_PROVIDER}" == "duo" ]; then 4 | echo "- Enabling DUO MFA" 5 | echo "#%PAM-1.0" > /etc/pam.d/mfa 6 | echo "auth include duo" >> /etc/pam.d/mfa 7 | if [ -z "${DUO_IKEY}" ]; then 8 | echo "DUO_IKEY not defined" 9 | exit 1; 10 | fi 11 | 12 | if [ -z "${DUO_SKEY}" ]; then 13 | echo "DUO_SKEY not defined" 14 | exit 1; 15 | fi 16 | 17 | if [ -z "${DUO_HOST}" ]; then 18 | echo "DUO_HOST not defined" 19 | exit 1; 20 | fi 21 | 22 | envsubst < /etc/pam_duo.conf.env > /etc/pam_duo.conf 23 | chmod 600 /etc/pam_duo.conf 24 | fi 25 | 26 | 27 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## what 2 | * Describe high-level what changed as a result of these commits (i.e. in plain-english, what do these changes mean?) 3 | * Use bullet points to be concise and to the point. 4 | 5 | ## why 6 | * Provide the justifications for the changes (e.g. business case). 7 | * Describe why these changes were made (e.g. why do these commits fix the problem?) 8 | * Use bullet points to be concise and to the point. 9 | 10 | ## references 11 | * Link to any supporting github issues or helpful documentation to add some context (e.g. stackoverflow). 12 | * Use `closes #123`, if this PR closes a GitHub issue `#123` 13 | 14 | -------------------------------------------------------------------------------- /examples/compose/gak.env.example: -------------------------------------------------------------------------------- 1 | GITHUB_API_TOKEN= 2 | GITHUB_ORGANIZATION= 3 | GITHUB_TEAM= 4 | SYNC_USERS_GID=500 5 | SYNC_USERS_GROUPS=sudo 6 | SYNC_USERS_SHELL=/usr/bin/sudosh 7 | SYNC_USERS_ROOT=/ 8 | SYNC_USERS_INTERVAL=60 9 | ETCD_ENDPOINT=http://etcd:2379 10 | ETCD_TTL=86400 11 | ETCD_PREFIX=github-authorized-keys 12 | LISTEN=:301 13 | INTEGRATE_SSH=false 14 | LOG_LEVEL=debug 15 | LINUX_USER_ADD_TPL=adduser -D -s {shell} {username} 16 | LINUX_USER_ADD_WITH_GID_TPL=adduser -D -s {shell} -u {gid} {username} 17 | LINUX_USER_ADD_TO_GROUP_TPL=addgroup {group} 18 | SSH_AUTHORIZED_KEYS_COMMAND_USER=root 19 | SSH_RESTART_TPL=echo "sshd restart" -------------------------------------------------------------------------------- /patches/openssh/cloudposse/original-command.diff: -------------------------------------------------------------------------------- 1 | diff --git a/session.c b/session.c 2 | index 89dcfdab..13b70ae8 100644 3 | --- a/session.c 4 | +++ b/session.c 5 | @@ -660,12 +660,11 @@ do_exec(struct ssh *ssh, Session *s, const char *command) 6 | const char *forced = NULL, *tty = NULL; 7 | char session_type[1024]; 8 | 9 | + original_command = command; 10 | if (options.adm_forced_command) { 11 | - original_command = command; 12 | command = options.adm_forced_command; 13 | forced = "(config)"; 14 | } else if (auth_opts->force_command != NULL) { 15 | - original_command = command; 16 | command = auth_opts->force_command; 17 | forced = "(key-option)"; 18 | } 19 | -------------------------------------------------------------------------------- /rootfs/etc/slack/pam-open_session-notification.json: -------------------------------------------------------------------------------- 1 | { "mrkdwn": true, 2 | "username": "${SLACK_USERNAME}", 3 | "attachments": [ 4 | { "mrkdwn_in": ["pretext", "fallback", "title"], 5 | "title": "SSH login on ${HOSTNAME}", 6 | "fallback": "login by ${PAM_USER}@${HOSTNAME} from ${PAM_RHOST}", 7 | "fields": [ 8 | { "title": "User", 9 | "value": "${PAM_USER}@${PAM_TTY}", 10 | "short": true 11 | }, 12 | { "title": "IP Address", 13 | "value": "${PAM_RHOST}", 14 | "short": true 15 | } 16 | ], 17 | "color": "#F35A00" 18 | } 19 | ], 20 | "icon_emoji": ":computer:" 21 | } 22 | -------------------------------------------------------------------------------- /rootfs/etc/init.d/rate-limit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "${RATE_LIMIT_ENABLED}" == "true" ]; then 4 | echo "- Enabling Rate Limits" 5 | echo "- Users will be locked for ${RATE_LIMIT_LOCKOUT_TIME}s after ${RATE_LIMIT_MAX_FAILURES} failed logins" 6 | echo "- Fail delay of ${RATE_LIMIT_FAIL_DELAY} micro-seconds" 7 | echo "#%PAM-1.0" > /etc/pam.d/rate-limit 8 | echo "auth requisite pam_faillock.so audit preauth deny=${RATE_LIMIT_MAX_FAILURES} even_deny_root unlock_time=${RATE_LIMIT_LOCKOUT_TIME}" >> /etc/pam.d/rate-limit 9 | echo "auth optional pam_faildelay.so delay=${RATE_LIMIT_FAIL_DELAY}" >> /etc/pam.d/rate-limit 10 | else 11 | echo "- Disabling Rate Limits" 12 | echo "#%PAM-1.0" > /etc/pam.d/rate-limit 13 | fi 14 | 15 | -------------------------------------------------------------------------------- /rootfs/etc/slack/ssh-force-command-notification.json: -------------------------------------------------------------------------------- 1 | { "mrkdwn": true, 2 | "username": "${SLACK_USERNAME}", 3 | "attachments": [ 4 | { "mrkdwn_in": ["text", "pretext", "fallback", "title"], 5 | "title": "SSH login on ${HOSTNAME}", 6 | "text": "```${SSH_ORIGINAL_COMMAND}```", 7 | "fallback": "login by ${SSH_USER}@${HOSTNAME} from ${SSH_CLIENT_IP}", 8 | "fields": [ 9 | { "title": "User", 10 | "value": "${SSH_USER}@${SSH_TERM}", 11 | "short": true 12 | }, 13 | { "title": "IP Address", 14 | "value": "${SSH_CLIENT_IP}", 15 | "short": true 16 | } 17 | ], 18 | "color": "#F35A00" 19 | } 20 | ], 21 | "icon_emoji": ":computer:" 22 | } 23 | -------------------------------------------------------------------------------- /rootfs/etc/profile: -------------------------------------------------------------------------------- 1 | export CHARSET=UTF-8 2 | export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 3 | export PAGER=less 4 | export PS1='\h:\w\$ ' 5 | 6 | # use nicer PS1 for bash and busybox ash 7 | if [ -n "$BASH_VERSION" -o "$BB_ASH_VERSION" ]; then 8 | PS1='\h:\w\$ ' 9 | # use nicer PS1 for zsh 10 | elif [ -n "$ZSH_VERSION" ]; then 11 | PS1='%m:%~%# ' 12 | # set up fallback default PS1 13 | else 14 | : "${HOSTNAME:=$(hostname)}" 15 | PS1='${HOSTNAME%%.*}:$PWD' 16 | [ "$(id -u)" -eq 0 ] && PS1="${PS1}# " || PS1="${PS1}\$ " 17 | fi 18 | 19 | for script in /etc/profile.d/*.sh ; do 20 | if [ -r $script ] ; then 21 | . $script 22 | if [ $? -ne 0 ]; then 23 | echo "Goodbye" 24 | exit 1 25 | fi 26 | fi 27 | done 28 | unset script 29 | -------------------------------------------------------------------------------- /test/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | bastion: 3 | build: 4 | context: ".." 5 | args: 6 | VERSION: "test" 7 | volumes: 8 | - "./fixtures/sshrc/sshrc_kill_test.sh:/etc/ssh/sshrc.d/sshrc_kill_test.sh" 9 | - "./fixtures/auth:/auth" 10 | - "./fixtures/server_scripts:/scripts" 11 | environment: 12 | LOG_LEVEL: "DEBUG" 13 | MFA_PROVIDER: "google-authenticator" 14 | healthcheck: 15 | test: ["CMD", "nc", "-z", "localhost", "22"] 16 | interval: 10s 17 | timeout: 5s 18 | retries: 3 19 | 20 | test: 21 | build: "." 22 | volumes: 23 | - "./fixtures/auth/ida_rsa:/root/.ssh/id_rsa" 24 | - "./fixtures/auth/google_authenticator_code:/code" 25 | - "./fixtures/client_scripts:/scripts" 26 | depends_on: 27 | - "bastion" 28 | -------------------------------------------------------------------------------- /patches/openssh/alpine/include-config-dir.patch: -------------------------------------------------------------------------------- 1 | --- a/ssh_config 2 | +++ b/ssh_config 3 | @@ -17,6 +17,10 @@ 4 | # list of available options, their meanings and defaults, please see the 5 | # ssh_config(5) man page. 6 | 7 | +# Include configuration snippets before processing this file to allow the 8 | +# snippets to override directives set in this file. 9 | +Include /etc/ssh/ssh_config.d/*.conf 10 | + 11 | # Host * 12 | # ForwardAgent no 13 | # ForwardX11 no 14 | --- a/sshd_config 15 | +++ b/sshd_config 16 | @@ -10,6 +10,10 @@ 17 | # possible, but leave them commented. Uncommented options override the 18 | # default value. 19 | 20 | +# Include configuration snippets before processing this file to allow the 21 | +# snippets to override directives set in this file. 22 | +Include /etc/ssh/sshd_config.d/*.conf 23 | + 24 | #Port 22 25 | #AddressFamily any 26 | #ListenAddress 0.0.0.0 27 | -------------------------------------------------------------------------------- /patches/openssh/alpine/fix-utmp.patch: -------------------------------------------------------------------------------- 1 | diff -rNU3 openssh-9.0p1.old/loginrec.c openssh-9.0p1/loginrec.c 2 | --- openssh-9.0p1.old/loginrec.c 2022-04-06 02:47:48.000000000 +0200 3 | +++ openssh-9.0p1/loginrec.c 2022-07-11 14:59:44.848827188 +0200 4 | @@ -763,10 +763,6 @@ 5 | set_utmpx_time(li, utx); 6 | utx->ut_pid = li->pid; 7 | 8 | - /* strncpy(): Don't necessarily want null termination */ 9 | - strncpy(utx->ut_user, li->username, 10 | - MIN_SIZEOF(utx->ut_user, li->username)); 11 | - 12 | if (li->type == LTYPE_LOGOUT) 13 | return; 14 | 15 | @@ -775,6 +771,10 @@ 16 | * for logouts. 17 | */ 18 | 19 | + /* strncpy(): Don't necessarily want null termination */ 20 | + strncpy(utx->ut_user, li->username, 21 | + MIN_SIZEOF(utx->ut_user, li->username)); 22 | + 23 | # ifdef HAVE_HOST_IN_UTMPX 24 | strncpy(utx->ut_host, li->hostname, 25 | MIN_SIZEOF(utx->ut_host, li->hostname)); 26 | -------------------------------------------------------------------------------- /rootfs/usr/bin/github-authorized-keys: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright 2017 Cloud Posse, LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -ue 18 | API_URL="${API_URL:-http://localhost:301/user/%s/authorized_keys}" 19 | if [ -n "$1" ]; then 20 | exec curl --silent --fail $(printf "$API_URL" "$1") 21 | else 22 | echo "Usage: $0 [github username]" 23 | fi 24 | 25 | -------------------------------------------------------------------------------- /patches/openssh/alpine/sshd-session-flavor.patch: -------------------------------------------------------------------------------- 1 | From 561a9fd3abf044b5dfafd4852097c1bccc8e98bb Mon Sep 17 00:00:00 2001 2 | From: Dominique Martinet 3 | Date: Mon, 1 Jul 2024 18:19:12 +0900 4 | Subject: [PATCH] allow using a different sshd-session flavor 5 | 6 | --- 7 | Makefile.in | 2 +- 8 | 1 file changed, 1 insertion(+), 1 deletion(-) 9 | 10 | diff --git a/Makefile.in b/Makefile.in 11 | index e1b77ebc6495..df745d80863b 100644 12 | --- a/Makefile.in 13 | +++ b/Makefile.in 14 | @@ -24,7 +24,7 @@ SSH_PROGRAM=@bindir@/ssh 15 | ASKPASS_PROGRAM=$(libexecdir)/ssh-askpass 16 | SFTP_SERVER=$(libexecdir)/sftp-server 17 | SSH_KEYSIGN=$(libexecdir)/ssh-keysign 18 | -SSHD_SESSION=$(libexecdir)/sshd-session 19 | +SSHD_SESSION=$(libexecdir)/sshd-session$(SSHD_SESSION_FLAVOR) 20 | SSH_PKCS11_HELPER=$(libexecdir)/ssh-pkcs11-helper 21 | SSH_SK_HELPER=$(libexecdir)/ssh-sk-helper 22 | PRIVSEP_PATH=@PRIVSEP_PATH@ 23 | -- 24 | 2.39.2 25 | 26 | -------------------------------------------------------------------------------- /rootfs/usr/bin/enforcer: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2017 Cloud Posse, LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | function _sigint() { 18 | echo -e '\nAborted' 19 | exit 1 20 | } 21 | 22 | trap _sigint SIGINT 23 | 24 | for script in /etc/enforce.d/* ; do 25 | if [ -r $script ] && [ -x $script ]; then 26 | $script 27 | if [ $? -ne 0 ]; then 28 | echo "Access denied" 29 | exit 1 30 | fi 31 | fi 32 | done 33 | -------------------------------------------------------------------------------- /test/fixtures/server_scripts/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This script runs on the test bastion server to initialize and setup the test environment. 4 | syslogd 5 | 6 | # Setup expect for google auth test 7 | apk update 8 | apk add expect 9 | 10 | rm -rf /var/log/sudo-io 11 | 12 | useradd -m bastion 13 | usermod -s /usr/bin/sudosh bastion 14 | cp /auth/google_authenticator /home/bastion/.google_authenticator 15 | chmod 600 /home/bastion/.google_authenticator 16 | mkdir /home/bastion/.ssh 17 | cp /auth/ida_rsa.pub /home/bastion/.ssh/authorized_keys 18 | chown -R bastion: /home/bastion 19 | 20 | # setup ejected user 21 | 22 | useradd -m sshrc_exit_test 23 | usermod -s /usr/bin/sudosh sshrc_exit_test 24 | cp /auth/google_authenticator /home/sshrc_exit_test/.google_authenticator 25 | chmod 600 /home/sshrc_exit_test/.google_authenticator 26 | mkdir /home/sshrc_exit_test/.ssh 27 | cp /auth/ida_rsa.pub /home/sshrc_exit_test/.ssh/authorized_keys 28 | chown -R sshrc_exit_test: /home/sshrc_exit_test 29 | 30 | echo "Setup complete" 31 | -------------------------------------------------------------------------------- /rootfs/usr/bin/fc: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2017 Cloud Posse, LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # 18 | # SSH Force Command (fc) Wrapper 19 | # 20 | 21 | function _sigint() { 22 | echo -e '\nAborted' 23 | exit 1 24 | } 25 | 26 | trap _sigint SIGINT 27 | 28 | for script in /etc/fc.d/* ; do 29 | if [ -r $script ] && [ -x $script ]; then 30 | . $script 31 | if [ $? -ne 0 ]; then 32 | echo "Access denied" 33 | exit 1 34 | fi 35 | fi 36 | done 37 | -------------------------------------------------------------------------------- /rootfs/usr/bin/sudosh-add-user: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2017 Cloud Posse, LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -e 18 | 19 | if [ -z "${PAM_USER}" ]; then 20 | echo "ERROR: invalid execution" 21 | exit 1 22 | fi 23 | 24 | SUDOERS_FILE="/etc/sudoers.d/sudosh-${PAM_USER}" 25 | 26 | if [ ! -f "${SUDOERS_FILE}" ]; then 27 | # Allow users to sudo to themselves; this does NOT allow root access 28 | echo "${PAM_USER} ALL=(${PAM_USER}) ALL" > "${SUDOERS_FILE}" 29 | fi 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Found a bug? Maybe our [Slack Community](https://slack.cloudposse.com) can help. 11 | 12 | [![Slack Community](https://slack.cloudposse.com/badge.svg)](https://slack.cloudposse.com) 13 | 14 | ## Describe the Bug 15 | A clear and concise description of what the bug is. 16 | 17 | ## Expected Behavior 18 | A clear and concise description of what you expected to happen. 19 | 20 | ## Steps to Reproduce 21 | Steps to reproduce the behavior: 22 | 1. Go to '...' 23 | 2. Run '....' 24 | 3. Enter '....' 25 | 4. See error 26 | 27 | ## Screenshots 28 | If applicable, add screenshots or logs to help explain your problem. 29 | 30 | ## Environment (please complete the following information): 31 | 32 | Anything that will help us triage the bug will help. Here are some ideas: 33 | - OS: [e.g. Linux, OSX, WSL, etc] 34 | - Version [e.g. 10.15] 35 | 36 | ## Additional Context 37 | Add any other context about the problem here. -------------------------------------------------------------------------------- /examples/compose/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | volumes: 3 | home: 4 | etc: 5 | services: 6 | bastion: 7 | image: cloudposse/bastion 8 | ports: 9 | - "1234:22" 10 | env_file: 11 | - bastion.env 12 | volumes: 13 | - home:/home 14 | - etc:/etc 15 | - "./scripts/ssh-authorized-keys-command:/etc/init.d/ssh-authorized-keys-command" 16 | - "./scripts/ssh-api-url:/etc/init.d/ssh-api-url" 17 | - "./scripts/ssh-log-level:/etc/init.d/ssh-log-level" 18 | gak: 19 | image: cloudposse/github-authorized-keys 20 | ports: 21 | - "301:301" 22 | volumes: 23 | - home:/home 24 | - etc:/etc 25 | env_file: 26 | - gak.env 27 | links: 28 | - "etcd:etcd" 29 | restart: always 30 | etcd: 31 | image: quay.io/coreos/etcd:v2.3.7 32 | command: 33 | - "--advertise-client-urls=http://0.0.0.0:2379,http://0.0.0.0:4001" 34 | - "--listen-client-urls=http://0.0.0.0:2379,http://0.0.0.0:4001" 35 | ports: 36 | - "2379:2379" 37 | - "2380:2380" 38 | - "4001:4001" 39 | - "7001:7001" 40 | restart: always -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'feature request' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Have a question? Please checkout our [Slack Community](https://slack.cloudposse.com) or visit our [Slack Archive](https://archive.sweetops.com/). 11 | 12 | [![Slack Community](https://slack.cloudposse.com/badge.svg)](https://slack.cloudposse.com) 13 | 14 | ## Describe the Feature 15 | 16 | A clear and concise description of what the bug is. 17 | 18 | ## Expected Behavior 19 | 20 | A clear and concise description of what you expected to happen. 21 | 22 | ## Use Case 23 | 24 | Is your feature request related to a problem/challenge you are trying to solve? Please provide some additional context of why this feature or capability will be valuable. 25 | 26 | ## Describe Ideal Solution 27 | 28 | A clear and concise description of what you want to happen. If you don't know, that's okay. 29 | 30 | ## Alternatives Considered 31 | 32 | Explain what alternative solutions or features you've considered. 33 | 34 | ## Additional Context 35 | 36 | Add any other context or screenshots about the feature request here. 37 | -------------------------------------------------------------------------------- /patches/README.md: -------------------------------------------------------------------------------- 1 | # OpenSSH patches 2 | 3 | OpenSSH will not compile out-of-the-box on alpine. For this reason, we use the official patches found here: 4 | 5 | - [https://git.alpinelinux.org/cgit/aports/tree/main/openssh](https://git.alpinelinux.org/cgit/aports/tree/main/openssh) 6 | 7 | We also add a couple of our own patches. 8 | 9 | One patch ensures we have `SSH_ORIGINAL_COMMAND` available during pam auth so we can send slack notifications. 10 | [original-command.diff](openssh/cloudposse/original-command.diff) 11 | 12 | The other patch obscures the version of OpenSSH. We use this to hide the SSH version so it's not announced to port-scanners. 13 | [obfuscate-version.diff](openssh/cloudposse/obfuscate-version.diff) 14 | 15 | Also we modified one alpine patch related to realpath, because it is outdated. 16 | [bsd-compatible-realpath.diff](openssh/cloudposse/bsd-compatible-realpath.diff) 17 | 18 | When upgrading version of OpenSSH, the patches might need to be regenerated. 19 | 20 | 21 | ## Dev Cheatsheet for Regenerating OpenSSH Patches 22 | 23 | ``` 24 | git clone --single-branch https://gitlab.alpinelinux.org/alpine/aports.git tmp 25 | cp tmp/main/openssh/*.patch patches/openssh/alpine/ 26 | ``` 27 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export DOCKER_IMAGE ?= cloudposse/$(APP) 2 | export DOCKER_TAG ?= dev 3 | export DOCKER_IMAGE_NAME ?= $(DOCKER_IMAGE):$(DOCKER_TAG) 4 | export DOCKER_BUILD_FLAGS = 5 | COPYRIGHT_SOFTWARE_DESCRIPTION := A secure Bastion host implemented as Docker Container running Alpine Linux with Google Authenticator & DUO MFA support 6 | 7 | .PHONY: test cleantest 8 | 9 | include $(shell curl --silent -O "https://raw.githubusercontent.com/cloudposse/build-harness/master/templates/Makefile.build-harness"; echo Makefile.build-harness) 10 | 11 | reset: 12 | ssh-keygen -R '[localhost]:1234' || true 13 | 14 | shell: reset 15 | docker run --name bastion --rm -it -p1234:22 \ 16 | -v ~/.ssh/:/root/.ssh/ \ 17 | --env-file=../.secrets \ 18 | --env-file=../.duo \ 19 | -e MFA_PROVIDER=google-authenticator \ 20 | -e SLACK_ENABLED=true \ 21 | --entrypoint=/bin/bash $(DOCKER_IMAGE_NAME) 22 | 23 | run: reset 24 | docker run --name bastion --rm -it -p1234:22 \ 25 | -v ~/.ssh/:/root/.ssh/ \ 26 | --env-file=../.secrets \ 27 | --env-file=../.duo \ 28 | -e MFA_PROVIDER=google-authenticator \ 29 | -e SLACK_ENABLED=true \ 30 | $(DOCKER_IMAGE_NAME) 31 | 32 | test: 33 | cd test && ./test.sh 34 | 35 | cleantest: 36 | cd test && docker compose down 37 | -------------------------------------------------------------------------------- /.github/auto-release.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION' 2 | tag-template: '$RESOLVED_VERSION' 3 | version-template: '$MAJOR.$MINOR.$PATCH' 4 | version-resolver: 5 | major: 6 | labels: 7 | - 'major' 8 | minor: 9 | labels: 10 | - 'minor' 11 | - 'enhancement' 12 | patch: 13 | labels: 14 | - 'auto-update' 15 | - 'patch' 16 | - 'fix' 17 | - 'bugfix' 18 | - 'bug' 19 | - 'hotfix' 20 | default: 'minor' 21 | filter-by-commitish: true 22 | 23 | categories: 24 | - title: '🚀 Enhancements' 25 | labels: 26 | - 'enhancement' 27 | - 'patch' 28 | - title: '🐛 Bug Fixes' 29 | labels: 30 | - 'fix' 31 | - 'bugfix' 32 | - 'bug' 33 | - 'hotfix' 34 | - title: '🤖 Automatic Updates' 35 | labels: 36 | - 'auto-update' 37 | 38 | change-template: | 39 |
40 | $TITLE @$AUTHOR (#$NUMBER) 41 | 42 | $BODY 43 |
44 | 45 | template: | 46 | $CHANGES 47 | 48 | replacers: 49 | # Remove irrelevant information from Renovate bot 50 | - search: '/(?<=---\s)\s*^#.*(Renovate configuration|Configuration)(?:.|\n)*?This PR has been generated .*/gm' 51 | replace: '' 52 | # Remove Renovate bot banner image 53 | - search: '/\[!\[[^\]]*Renovate\][^\]]*\](\([^)]*\))?\s*\n+/gm' 54 | replace: '' 55 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Use this file to define individuals or teams that are responsible for code in a repository. 2 | # Read more: 3 | # 4 | # Order is important: the last matching pattern has the highest precedence 5 | 6 | # These owners will be the default owners for everything 7 | * @cloudposse/engineering @cloudposse/contributors 8 | 9 | # Cloud Posse must review any changes to Makefiles 10 | **/Makefile @cloudposse/engineering 11 | **/Makefile.* @cloudposse/engineering 12 | 13 | # Cloud Posse must review any changes to GitHub actions 14 | .github/* @cloudposse/engineering 15 | 16 | # Cloud Posse must review any changes to standard context definition, 17 | # but some changes can be rubber-stamped. 18 | **/*.tf @cloudposse/engineering @cloudposse/contributors @cloudposse/approvers 19 | README.yaml @cloudposse/engineering @cloudposse/contributors @cloudposse/approvers 20 | README.md @cloudposse/engineering @cloudposse/contributors @cloudposse/approvers 21 | docs/*.md @cloudposse/engineering @cloudposse/contributors @cloudposse/approvers 22 | 23 | # Cloud Posse Admins must review all changes to CODEOWNERS or the mergify configuration 24 | .github/mergify.yml @cloudposse/admins 25 | .github/CODEOWNERS @cloudposse/admins 26 | -------------------------------------------------------------------------------- /rootfs/usr/bin/setup-google-authenticator: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2017 Cloud Posse, LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | until [ -f "${HOME}/.google_authenticator" ]; do 18 | if ! [[ -t 1 ]] || [[ "$SSH_ORIGINAL_COMMAND" =~ ^(rsync|nc|scp) ]]; then 19 | echo "MFA setup required" 20 | exit 1 21 | else 22 | echo -e "\nWelcome $USER. Please follow the prompts to setup your MFA device..." 23 | umask 0066 24 | google-authenticator 25 | if [ $? -ne 0 ]; then 26 | echo "MFA setup failed" 27 | exit 1; 28 | fi 29 | 30 | if [ -f .google_authenticator ]; then 31 | chmod 600 .google_authenticator 32 | echo "MFA enabled for $USER" 33 | else 34 | echo "MFA setup is mandatory" 35 | fi 36 | fi 37 | done 38 | 39 | trap - SIGINT 40 | -------------------------------------------------------------------------------- /.github/workflows/release-published.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: release 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | permissions: 9 | id-token: write 10 | contents: write 11 | packages: write 12 | 13 | jobs: 14 | terraform-module: 15 | uses: cloudposse/.github/.github/workflows/shared-release-branches.yml@main 16 | secrets: inherit 17 | 18 | ci-build-push: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v3 23 | with: 24 | fetch-depth: 0 25 | 26 | - name: Build 27 | id: build 28 | uses: cloudposse/github-action-docker-build-push@main 29 | with: 30 | registry: registry.hub.docker.com 31 | organization: "${{ github.event.repository.owner.login }}" 32 | repository: "${{ github.event.repository.name }}" 33 | login: "${{ secrets.DOCKERHUB_USERNAME }}" 34 | password: "${{ secrets.DOCKERHUB_PASSWORD }}" 35 | 36 | - name: Build GHR 37 | id: build-ghr 38 | uses: cloudposse/github-action-docker-build-push@main 39 | with: 40 | registry: ghcr.io 41 | organization: "${{ github.event.repository.owner.login }}" 42 | repository: "${{ github.event.repository.name }}" 43 | login: "${{ github.actor }}" 44 | password: "${{ secrets.GITHUB_TOKEN }}" 45 | -------------------------------------------------------------------------------- /patches/openssh/alpine/disable-fzero-call-used-regs-used-on-ppc64le.patch: -------------------------------------------------------------------------------- 1 | From 2b340fc84c292db1272ae4b3a7eb85a3de223ddd Mon Sep 17 00:00:00 2001 2 | From: Dominique Martinet 3 | Date: Tue, 2 Jul 2024 10:00:38 +0900 4 | Subject: [PATCH] disable -fzero-call-used-regs=used on ppc64le 5 | 6 | This fails as follow for some files: 7 | packet.c: In function 'ssh_packet_log_type': 8 | packet.c:1158:1: sorry, unimplemented: argument 'used' is not supported for '-fzero-call-used-regs' on this target 9 | 1158 | } 10 | | ^ 11 | make: *** [Makefile:203: packet.o] Error 1 12 | 13 | This had previously been an issue on an older version as well and the 14 | "fix" at the time was to make the detection function more likely to 15 | trigger that behaviour, but that was apparently not enough so just 16 | disable at configure level. 17 | 18 | Link: https://issues.guix.gnu.org/68212 19 | --- 20 | configure.ac | 12 ++++++++---- 21 | 1 file changed, 8 insertions(+), 4 deletions(-) 22 | 23 | diff --git a/configure.ac b/configure.ac 24 | index 5a865f8e1b07..4f99ad35a3ce 100644 25 | --- a/configure.ac 26 | +++ b/configure.ac 27 | @@ -237,10 +237,14 @@ if test "$GCC" = "yes" || test "$GCC" = "egcs"; then 28 | # https://github.com/llvm/llvm-project/issues/59242 29 | # clang 17 has a different bug that causes an ICE when using this 30 | # flag at all (https://bugzilla.mindrot.org/show_bug.cgi?id=3629) 31 | - case "$CLANG_VER" in 32 | - apple-15*) OSSH_CHECK_CFLAG_LINK([-fzero-call-used-regs=used]) ;; 33 | - 17*) ;; 34 | - *) OSSH_CHECK_CFLAG_LINK([-fzero-call-used-regs=used]) ;; 35 | + case "$host" in 36 | + "powerpc64le"*) ;; # skip on ppc64le 37 | + *) 38 | + case "$CLANG_VER" in 39 | + apple-15*) OSSH_CHECK_CFLAG_LINK([-fzero-call-used-regs=used]) ;; 40 | + 17*) ;; 41 | + *) OSSH_CHECK_CFLAG_LINK([-fzero-call-used-regs=used]) ;; 42 | + esac 43 | esac 44 | OSSH_CHECK_CFLAG_COMPILE([-ftrivial-auto-var-init=zero]) 45 | fi 46 | -- 47 | 2.39.2 48 | 49 | -------------------------------------------------------------------------------- /test/fixtures/server_scripts/google-auth.exp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/expect -f 2 | # 3 | # This Expect script was generated by autoexpect on Mon May 5 15:07:42 2025 4 | # Expect and autoexpect were both written by Don Libes, NIST. 5 | # 6 | # Note that autoexpect does not guarantee a working script. It 7 | # necessarily has to guess about certain things. Two reasons a script 8 | # might fail are: 9 | # 10 | # 1) timing - A surprising number of programs (rn, ksh, zsh, telnet, 11 | # etc.) and devices discard or ignore keystrokes that arrive "too 12 | # quickly" after prompts. If you find your new script hanging up at 13 | # one spot, try adding a short sleep just before the previous send. 14 | # Setting "force_conservative" to 1 (see below) makes Expect do this 15 | # automatically - pausing briefly before sending each character. This 16 | # pacifies every program I know of. The -c flag makes the script do 17 | # this in the first place. The -C flag allows you to define a 18 | # character to toggle this mode off and on. 19 | 20 | set force_conservative 0 ;# set to 1 to force conservative mode even if 21 | ;# script wasn't run conservatively originally 22 | if {$force_conservative} { 23 | set send_slow {1 .1} 24 | proc send {ignore arg} { 25 | sleep .1 26 | exp_send -s -- $arg 27 | } 28 | } 29 | 30 | # 31 | # 2) differing output - Some programs produce different output each time 32 | # they run. The "date" command is an obvious example. Another is 33 | # ftp, if it produces throughput statistics at the end of a file 34 | # transfer. If this causes a problem, delete these patterns or replace 35 | # them with wildcards. An alternative is to use the -p flag (for 36 | # "prompt") which makes Expect only look for the last line of output 37 | # (i.e., the prompt). The -P flag allows you to define a character to 38 | # toggle this mode off and on. 39 | # 40 | # Read the man page for more info. 41 | # 42 | # -Don 43 | 44 | 45 | set timeout -1 46 | spawn google-authenticator -t 47 | match_max 100000 48 | expect "Enter code from app (-1 to skip): " 49 | send -- "-1\r" 50 | expect "Do you want me to update your \"/root/.google_authenticator\" file? (y/n) " 51 | send -- "n\r" 52 | expect eof 53 | -------------------------------------------------------------------------------- /rootfs/etc/ssh/sshd_config: -------------------------------------------------------------------------------- 1 | Port 22 2 | AddressFamily any 3 | ListenAddress 0.0.0.0 4 | ListenAddress :: 5 | 6 | Protocol 2 7 | HostKey /etc/ssh/ssh_host_rsa_key 8 | 9 | # Lifetime and size of ephemeral version 1 server key 10 | #KeyRegenerationInterval 1h 11 | #ServerKeyBits 1024 12 | 13 | # Ciphers and keying 14 | #RekeyLimit default none 15 | 16 | # Logging 17 | # obsoletes QuietMode and FascistLogging 18 | #SyslogFacility AUTH 19 | #LogLevel INFO 20 | 21 | # Authentication: 22 | 23 | LoginGraceTime 2m 24 | PermitRootLogin yes 25 | PermitUserRC no 26 | StrictModes no 27 | MaxAuthTries 6 28 | MaxSessions 1 29 | 30 | PubkeyAuthentication yes 31 | 32 | # The default is to check both .ssh/authorized_keys and .ssh/authorized_keys2 33 | # but this is overridden so installations will only check .ssh/authorized_keys 34 | AuthorizedKeysFile .ssh/authorized_keys 35 | 36 | #AuthorizedPrincipalsFile none 37 | 38 | # Don't read the user's ~/.rhosts and ~/.shosts files 39 | IgnoreRhosts yes 40 | 41 | # To disable tunneled clear text passwords, change to no here! 42 | #PasswordAuthentication yes 43 | PermitEmptyPasswords no 44 | 45 | # Change to no to disable s/key passwords 46 | ChallengeResponseAuthentication yes 47 | 48 | UsePAM yes 49 | AuthenticationMethods publickey,keyboard-interactive 50 | AllowAgentForwarding yes 51 | AllowTcpForwarding no 52 | GatewayPorts no 53 | X11Forwarding no 54 | PermitTTY yes 55 | PrintMotd no 56 | PrintLastLog yes 57 | TCPKeepAlive yes 58 | #UseLogin no 59 | PermitUserEnvironment no 60 | #Compression delayed 61 | ClientAliveInterval 30 62 | ClientAliveCountMax 3 63 | UseDNS no 64 | #PidFile /run/sshd.pid 65 | PermitTunnel yes 66 | ChrootDirectory none 67 | VersionAddendum none 68 | 69 | # no default banner path 70 | Banner none 71 | 72 | # override default of no subsystems 73 | Subsystem sftp /usr/lib/ssh/sftp-server -l VERBOSE 74 | 75 | # the following are HPN related configuration options 76 | # tcp receive buffer polling. disable in non autotuning kernels 77 | #TcpRcvBufPoll yes 78 | 79 | # disable hpn performance boosts 80 | #HPNDisabled no 81 | 82 | # buffer size for hpn to non-hpn connections 83 | #HPNBufferSize 2048 84 | 85 | ForceCommand /usr/bin/fc 86 | 87 | # Example of overriding settings on a per-user basis 88 | #Match User anoncvs 89 | # X11Forwarding no 90 | # AllowTcpForwarding no 91 | # PermitTTY no 92 | # ForceCommand cvs server 93 | -------------------------------------------------------------------------------- /test/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export TERM=linux 4 | 5 | red=`tput setaf 1` 6 | green=`tput setaf 2` 7 | reset=`tput sgr0` 8 | 9 | # Generating temp keys 10 | rm -rf fixtures/auth/ida_rsa* 11 | ssh-keygen -q -f fixtures/auth/ida_rsa -N "" 12 | chmod 600 fixtures/auth/ida_rsa 13 | 14 | docker compose down 15 | docker compose up -d --build bastion 16 | docker compose build test 17 | 18 | # wait until bastion is up 19 | until docker compose exec bastion ps aux|grep -v grep| grep sshd > /dev/null; do echo "Waiting for bastion to come online..."; sleep 1; done 20 | 21 | echo "Bastion sshd service started." 22 | 23 | docker compose exec bastion /scripts/setup.sh 24 | 25 | 26 | # greping for the first line of the left alignment square in the generated QR Code 27 | docker compose exec bastion /scripts/google_auth_qr_code_generator_test.sh |grep -F " " > /dev/null 28 | 29 | retVal=$? 30 | 31 | if [ $retVal -ne 0 ]; then 32 | echo "${red}* Google Authenticator QR Code Generator Test Failed${reset}" 33 | exit $retVal 34 | else 35 | echo "${green}* Google Authenticator QR Code Generator Test Succeeded${reset}" 36 | fi 37 | 38 | 39 | docker compose run --remove-orphans test /scripts/google_auth_test.sh 40 | 41 | retVal=$? 42 | 43 | if [ $retVal -ne 0 ]; then 44 | echo "${red}* Google Authenticator/SSH Test Failed${reset}" 45 | exit $retVal 46 | else 47 | echo "${green}* Google Authenticator/SSH Test Succeeded${reset}" 48 | fi 49 | 50 | 51 | docker compose exec bastion ls /var/log/sudo-io/00/00/01/ 52 | 53 | retVal=$? 54 | if [ $retVal -ne 0 ]; then 55 | echo "${red}* sudosh Audit Failed - no logs created!${reset}" 56 | exit $retVal 57 | else 58 | echo "${green}* sudosh Audit Test Succeeded${reset}" 59 | fi 60 | 61 | 62 | docker compose exec bastion curl https://hooks.slack.com 63 | 64 | retVal=$? 65 | 66 | if [ $retVal -ne 0 ]; then 67 | echo "${red}* Failure to connect to slack API.${reset}" 68 | exit $retVal 69 | else 70 | echo "${green}* Slack API Connection Test Succeeded${reset}" 71 | fi 72 | 73 | export SSHRC_KILL_OUTPUT=`docker compose run --remove-orphans test /scripts/sshrc_kill_test.sh` 74 | 75 | if [[ "$SSHRC_KILL_OUTPUT" == *"this output should never print"* ]]; then 76 | echo "${red}* Failure to quit after non-zero exit code in sshrc${reset}" 77 | exit 1 78 | else 79 | echo "${green}* sshrc non-zero exit code quit Succeeded${reset}" 80 | fi 81 | 82 | -------------------------------------------------------------------------------- /rootfs/usr/bin/slack-notification: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2017 Cloud Posse, LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | if [ -f /etc/slack/env ]; then 18 | . /etc/slack/env 19 | fi 20 | 21 | set -o pipefail 22 | 23 | function debug() { 24 | if [ "${DEBUG}" == "true" ]; then 25 | echo "DEBUG: $*" >&2 26 | fi 27 | } 28 | 29 | if [ -n "${SSH_CLIENT}" ]; then 30 | # Invoked as an SSH ForceCommand 31 | export SSH_USER="${USER}" 32 | export SSH_CLIENT_IP="$(echo "$SSH_CONNECTION" | cut -d' ' -f1)" 33 | export SSH_TERM="tty${SSH_TTY##*/}" 34 | if [ "${SSH_TERM}" == "tty" ]; then 35 | export SSH_TERM="notty" 36 | fi 37 | export SSH_ORIGINAL_COMMAND="${SSH_ORIGINAL_COMMAND:-shell}" 38 | export SLACK_NOTIFICATION_TEMPLATE="${SLACK_NOTIFICATION_TEMPLATE:-/etc/slack/ssh-force-command-notification.json}" 39 | elif [ "${PAM_TYPE}" == "open_session" ]; then 40 | # Invoked as a PAM module 41 | export SLACK_NOTIFICATION_TEMPLATE="${SLACK_NOTIFICATION_TEMPLATE:-/etc/slack/pam-${PAM_TYPE}-notification.json}" 42 | else 43 | echo "Unknown invocation" >&2 44 | exit 1 45 | fi 46 | 47 | if [ -s "/etc/hostname" ]; then 48 | export HOSTNAME=`cat /etc/hostname` 49 | else 50 | export HOSTNAME=`hostname` 51 | fi 52 | export SHORT_HOSTNAME="${HOSTNAME%%.*}" 53 | export SLACK_USERNAME="${SLACK_USERNAME:-${SHORT_HOSTNAME}}" 54 | export SLACK_TIMEOUT="${SLACK_TIMEOUT:-2}" 55 | export SLACK_FATAL_ERRORS="${SLACK_FATAL_ERRORS:-true}" 56 | 57 | if [ -z "${SLACK_WEBHOOK_URL}" ]; then 58 | echo "SLACK_WEBHOOK_URL not defined" 59 | exit 1 60 | fi 61 | 62 | # See: https://api.slack.com/docs/messages/builder 63 | if [ -f "${SLACK_NOTIFICATION_TEMPLATE}" ]; then 64 | envsubst < "${SLACK_NOTIFICATION_TEMPLATE}" | \ 65 | curl --fail --silent --connect-timeout ${SLACK_TIMEOUT} -H 'Content-type: application/json' --data @- "${SLACK_WEBHOOK_URL}" >/dev/null 66 | 67 | exit=$? 68 | 69 | if [ $exit -eq 0 ]; then 70 | debug "Slack notification sent" 71 | exit $exit 72 | elif [ "${SLACK_FATAL_ERRORS}" == "true" ]; then 73 | debug "Slack notification failed" 74 | exit $exit 75 | fi 76 | fi 77 | -------------------------------------------------------------------------------- /examples/compose/README.md: -------------------------------------------------------------------------------- 1 | # Bastion example using docker-compose 2 | 3 | This example starts up cloudposse bastion, github-authorized-keys and etcd. 4 | 5 | ### Requirements 6 | 1. You will need to [install docker-compose](https://docs.docker.com/compose/install/). 7 | 2. Have an [SSH key added to your github account](https://help.github.com/en/articles/adding-a-new-ssh-key-to-your-github-account). 8 | ##### Recommended 9 | Create a slack webhook. Follow this simple [guide](https://api.slack.com/tutorials/slack-apps-hello-world). 10 | Copy `bastion.env.example` to `bastion.env` and set the following variable; 11 | ``` 12 | SLACK_WEBHOOK_URL= 13 | ``` 14 | 15 | Obtain the GitHub API Token (aka Personal Access Token) [here](https://github.com/settings/tokens). Click "Generate new token" and select `read:org`. 16 | Create a team [here](https://help.github.com/en/articles/creating-a-team). 17 | Copy `gak.env.example` to `gak.env` and set the following variables; 18 | ``` 19 | GITHUB_API_TOKEN= 20 | GITHUB_ORGANIZATION= 21 | GITHUB_TEAM= 22 | ``` 23 | ### Start the stack 24 | To start, run 25 | ``` 26 | bastion/examples/compose$ docker-compose up -d 27 | ``` 28 | 29 | ### Connect to bastion 30 | Connect to bastion via ssh by running. 31 | ``` 32 | bastion/examples/compose$ ssh @ -p 1234 33 | ``` 34 | may be one of the following; 35 | 1. localhost 36 | 2. `bastion/examples/compose$ docker-machine ip` 37 | 38 | Make sure you substitute the appropriate values. 39 | 40 | ### Check status 41 | Check the status of your containers by running; 42 | ``` 43 | bastion/examples/compose$ docker-compose ps 44 | ``` 45 | Your output should look like this 46 | ```sh 47 | Name Command State Ports 48 | ----------------------------------------------------------------------------------------------------------------------------------------------------------- 49 | compose_bastion_1 /init Up 0.0.0.0:1234->22/tcp 50 | compose_etcd_1 /etcd --advertise-client-u ... Up 0.0.0.0:2379->2379/tcp, 0.0.0.0:2380->2380/tcp, 0.0.0.0:4001->4001/tcp, 0.0.0.0:7001->7001/tcp 51 | compose_gak_1 github-authorized-keys Up 0.0.0.0:301->301/tcp 52 | 53 | ``` 54 | 55 | ### Clean up 56 | To stop the containers and remove attached volumes, run; 57 | ``` 58 | bastion/examples/compose$ docker-compose down -v 59 | ``` 60 | 61 | ### Build from source 62 | To stop the containers and remove attached volumes, run; 63 | ``` 64 | bastion/examples/compose$ docker-compose down -v 65 | ``` 66 | 67 | ## References 68 | https://github.com/cloudposse/github-authorized-keys 69 | 70 | 71 | ## References 72 | https://github.com/cloudposse/github-authorized-keys 73 | https://help.github.com/en/articles/adding-a-new-ssh-key-to-your-github-account 74 | https://api.slack.com/tutorials/slack-apps-hello-world 75 | https://help.github.com/en/articles/creating-a-team -------------------------------------------------------------------------------- /.github/workflows/feature-branch.yml: -------------------------------------------------------------------------------- 1 | name: Branch 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | - release/** 7 | types: [opened, synchronize, reopened, labeled, unlabeled] 8 | push: 9 | branches: 10 | - main 11 | - release/** 12 | paths-ignore: 13 | - '.github/**' 14 | - 'docs/**' 15 | - 'examples/**' 16 | - 'test/**' 17 | 18 | permissions: 19 | pull-requests: write 20 | id-token: write 21 | contents: read 22 | packages: write 23 | 24 | jobs: 25 | ci-readme: 26 | uses: cloudposse/.github/.github/workflows/shared-readme.yml@main 27 | if: ${{ github.event_name == 'push' }} 28 | secrets: inherit 29 | 30 | ci-codeowners: 31 | uses: cloudposse/.github/.github/workflows/shared-codeowners.yml@main 32 | with: 33 | is_fork: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }} 34 | secrets: inherit 35 | 36 | ci-labels: 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: cloudposse/github-action-release-label-validator@v1 40 | 41 | test: 42 | runs-on: ubuntu-latest 43 | steps: 44 | - name: Checkout 45 | uses: actions/checkout@v3 46 | 47 | - uses: KengoTODA/actions-setup-docker-compose@main 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | 51 | - name: Run Tests 52 | shell: bash 53 | run: make test 54 | 55 | - name: Cleanup 56 | if: always() 57 | shell: bash 58 | run: make cleantest 59 | 60 | ci: 61 | runs-on: ubuntu-latest 62 | if: ${{ always() }} 63 | steps: 64 | - run: | 65 | echo '${{ toJSON(needs) }}' # easier debug 66 | ! ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} 67 | needs: [ ci-readme, ci-codeowners, ci-labels, test ] 68 | 69 | ci-build-push: 70 | runs-on: ubuntu-latest 71 | if: ${{ github.event_name == 'push' }} 72 | needs: [ ci ] 73 | steps: 74 | - name: Checkout 75 | uses: actions/checkout@v3 76 | with: 77 | fetch-depth: 0 78 | 79 | - name: Build 80 | id: build 81 | uses: cloudposse/github-action-docker-build-push@main 82 | with: 83 | registry: registry.hub.docker.com 84 | organization: "${{ github.event.repository.owner.login }}" 85 | repository: "${{ github.event.repository.name }}" 86 | login: "${{ secrets.DOCKERHUB_USERNAME }}" 87 | password: "${{ secrets.DOCKERHUB_PASSWORD }}" 88 | 89 | - name: Build GHR 90 | id: build-ghr 91 | uses: cloudposse/github-action-docker-build-push@main 92 | with: 93 | registry: ghcr.io 94 | organization: "${{ github.event.repository.owner.login }}" 95 | repository: "${{ github.event.repository.name }}" 96 | login: "${{ github.actor }}" 97 | password: "${{ secrets.GITHUB_TOKEN }}" 98 | 99 | auto-release: 100 | name: "Release" 101 | needs: [ci, ci-build-push] 102 | uses: cloudposse/.github/.github/workflows/shared-auto-release.yml@main 103 | if: ${{ github.event_name == 'push' }} 104 | with: 105 | publish: true 106 | secrets: inherit 107 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.21 AS base 2 | 3 | ## 4 | ## Base builder image 5 | ## 6 | FROM base AS builder 7 | 8 | RUN apk --update add --virtual .build-deps build-base automake autoconf libtool git linux-pam-dev zlib-dev openssl-dev wget 9 | 10 | ## 11 | ## Duo builder image 12 | ## 13 | FROM builder AS duo-builder 14 | 15 | ARG DUO_VERSION=2.0.4 16 | RUN wget https://dl.duosecurity.com/duo_unix-${DUO_VERSION}.tar.gz && \ 17 | mkdir -p src && \ 18 | tar -zxf duo_unix-${DUO_VERSION}.tar.gz --strip-components=1 -C src 19 | 20 | RUN cd src && \ 21 | ./configure \ 22 | --with-pam=/dist/lib64/security \ 23 | --prefix=/dist/usr && \ 24 | make && \ 25 | make install 26 | 27 | ## 28 | ## Google Authenticator PAM module builder image 29 | ## 30 | FROM builder AS google-authenticator-libpam-builder 31 | 32 | ARG AUTHENTICATOR_LIBPAM_VERSION=1.11 33 | RUN git clone --branch ${AUTHENTICATOR_LIBPAM_VERSION} --single-branch https://github.com/google/google-authenticator-libpam src 34 | 35 | RUN cd src && \ 36 | ./bootstrap.sh && \ 37 | ./configure \ 38 | --prefix=/usr && \ 39 | make && \ 40 | make install 41 | 42 | ## 43 | ## OpenSSH Portable builder image 44 | ## 45 | FROM builder AS openssh-portable-builder 46 | 47 | ARG OPENSSH_VERSION=V_9_9_P2 48 | RUN git clone --branch ${OPENSSH_VERSION} --single-branch https://github.com/openssh/openssh-portable src 49 | 50 | COPY patches/ /patches/ 51 | 52 | RUN cd src && \ 53 | find ../patches/openssh/** -type f -exec patch -p1 -i {} \; && \ 54 | autoreconf && \ 55 | ./configure \ 56 | --prefix=/dist/usr \ 57 | --sysconfdir=/etc/ssh \ 58 | --datadir=/dist/usr/share/openssh \ 59 | --libexecdir=/usr/lib/ssh \ 60 | --mandir=/dist/usr/share/man \ 61 | --with-pid-dir=/run \ 62 | --with-mantype=man \ 63 | --with-privsep-path=/var/empty \ 64 | --with-privsep-user=sshd \ 65 | --with-ssl-engine \ 66 | --disable-wtmp \ 67 | --with-pam=/usr/lib64/security && \ 68 | make && \ 69 | make install 70 | 71 | ## 72 | ## Bastion image 73 | ## 74 | FROM base 75 | 76 | LABEL maintainer="erik@cloudposse.com" 77 | 78 | USER root 79 | 80 | ## Install dependencies 81 | RUN apk --update add curl drill groff util-linux bash xauth gettext openssl-dev shadow linux-pam libqrencode sudo && \ 82 | rm -rf /etc/ssh/ssh_host_*_key* && \ 83 | rm -f /usr/bin/ssh-agent && \ 84 | rm -f /usr/bin/ssh-keyscan && \ 85 | touch /var/log/lastlog && \ 86 | mkdir -p /var/run/sshd && \ 87 | ln -s /etc/profile.d/color_prompt.sh.disabled /etc/profile.d/color_prompt.sh 88 | 89 | ## Install sudosh 90 | ENV SUDOSH_VERSION=0.3.0 91 | RUN wget https://github.com/cloudposse/sudosh/releases/download/${SUDOSH_VERSION}/sudosh_linux_386 -O /usr/bin/sudosh && \ 92 | chmod 755 /usr/bin/sudosh 93 | 94 | ## Install Duo 95 | COPY --from=duo-builder dist/ / 96 | 97 | ## Install Google Authenticator PAM module 98 | COPY --from=google-authenticator-libpam-builder /usr /usr 99 | 100 | ## Install OpenSSH Portable 101 | COPY --from=openssh-portable-builder dist/ / 102 | COPY --from=openssh-portable-builder /usr/lib/ssh /usr/lib/ssh 103 | 104 | 105 | ## System 106 | ENV TIMEZONE="Etc/UTC" \ 107 | TERM="xterm" \ 108 | HOSTNAME="bastion" 109 | 110 | ENV MFA_PROVIDER="duo" 111 | 112 | ENV UMASK="0022" 113 | 114 | ## Duo 115 | ENV DUO_IKEY="" \ 116 | DUO_SKEY="" \ 117 | DUO_HOST="" \ 118 | DUO_FAILMODE="secure" \ 119 | DUO_AUTOPUSH="yes" \ 120 | DUO_PROMPTS="1" 121 | 122 | ## Enforcer 123 | ENV ENFORCER_ENABLED="true" \ 124 | ENFORCER_CLEAN_HOME_ENABLED="true" 125 | 126 | 127 | ## Enable Rate Limiting 128 | ENV RATE_LIMIT_ENABLED="true" 129 | 130 | ## Tolerate 5 consecutive failures 131 | ENV RATE_LIMIT_MAX_FAILURES="5" 132 | ## Lock accounts out for 300 seconds (5 minutes) after repeated failures 133 | ENV RATE_LIMIT_LOCKOUT_TIME="300" 134 | ## Sleep N microseconds between failed attempts 135 | ENV RATE_LIMIT_FAIL_DELAY="3000000" 136 | 137 | ## Slack 138 | ENV SLACK_ENABLED="false" \ 139 | SLACK_HOOK="sshrc" \ 140 | SLACK_WEBHOOK_URL="" \ 141 | SLACK_USERNAME="" \ 142 | SLACK_TIMEOUT="2" \ 143 | SLACK_FATAL_ERRORS="true" 144 | 145 | ## SSH 146 | ENV SSH_AUDIT_ENABLED="true" \ 147 | SSH_AUTHORIZED_KEYS_COMMAND="none" \ 148 | SSH_AUTHORIZED_KEYS_COMMAND_USER="nobody" 149 | 150 | ADD rootfs/ / 151 | 152 | EXPOSE 22 153 | ENTRYPOINT ["/init"] 154 | -------------------------------------------------------------------------------- /README.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # 3 | # This is the canonical configuration for the `README.md` 4 | # Run `make readme` to rebuild the `README.md` 5 | # 6 | 7 | # Name of this project 8 | name: bastion 9 | 10 | # Logo for this project 11 | #logo: docs/logo.png 12 | 13 | # License of this project 14 | license: "APACHE2" 15 | 16 | # Canonical GitHub repo 17 | github_repo: cloudposse/bastion 18 | 19 | # Badges to display 20 | badges: 21 | - name: "Latest Release" 22 | image: "https://img.shields.io/github/release/cloudposse/bastion.svg" 23 | url: "https://github.com/cloudposse/bastion/releases/latest" 24 | - name: "Build & Test Status" 25 | image: "https://github.com/cloudposse/bastion/actions/workflows/integration-tests.yml/badge.svg" 26 | url: "https://github.com/cloudposse/bastion/actions/workflows/integration-tests.yml" 27 | - name: "Slack Community" 28 | image: "https://slack.cloudposse.com/badge.svg" 29 | url: "https://slack.cloudposse.com" 30 | 31 | # Short description of this project 32 | description: |- 33 | This is a secure/locked-down bastion implemented as a Docker Container. It uses Alpine Linux as the base image and ships with support for Google Authenticator & DUO MFA support. 34 | 35 | It was designed to be used on Kubernetes together with [GitHub Authorized Keys](https://github.com/cloudposse/github-authorized-keys) to provide secure remote access to production clusters. 36 | ### MFA Setup & Usage 37 | 38 | Here's a demo of what a user experiences when setting up Google Authenticator for the first time. 39 | 40 | ![Demo 1](docs/demo.gif) 41 | 42 | When using Duo as the MFA provider, this becomes even more magical because Duo supports automatic Push notifications to your mobile device. 43 | Just approve the request on your mobile phone (e.g. with a thumb press on iOS) when prompted. 44 | 45 | ### Slack Notifications 46 | 47 | Here's what it looks like when someone connects to the bastion if Slack notifications are enabled. 48 | 49 | ![Demo 2](docs/slack.png) 50 | 51 | We recommend using Slack notifications for self-reporting. 52 | * Any time a user accesses production systems, they should reply to the slack notification to justify their remote access. 53 | * A "buddy" should approve the login by adding a reaction (e.g. ✅). 54 | * If no one approves the login, it should trigger an *incident response* to track down the unauthorized access. 55 | 56 | quickstart: |- 57 | 58 | Here's how you can quickly demo the `bastion`. We assume you have `~/.ssh/authorized_keys` properly configured and your SSH key (e.g. `~/.ssh/id_rsa`) added to your SSH agent. 59 | 60 | 61 | ```bash 62 | $ docker run -it -p 1234:22 \ 63 | -e MFA_PROVIDER=google-authenticator \ 64 | -v ~/.ssh/authorized_keys:/root/.ssh/authorized_keys \ 65 | cloudposse/bastion 66 | ``` 67 | 68 | Now, in another terminal you should be able to run: 69 | ```bash 70 | $ ssh root@localhost -p 1234 71 | ``` 72 | 73 | The first time you connect, you'll be asked to setup your MFA device. Subsequently, each time you connect, you'll be prompted to enter your MFA token. 74 | 75 | # How to use this project 76 | usage: |- 77 | ### Running 78 | 79 | Refer to the [Environment Variables](#environment-variables) section below to tune how the `bastion` operates. 80 | 81 | 82 | ```bash 83 | $ docker run -p 1234:22 cloudposse/bastion:latest 84 | ``` 85 | 86 | ### Building 87 | 88 | ```bash 89 | $ git clone https://github.com/cloudposse/bastion.git 90 | $ cd bastion 91 | $ make docker:build 92 | ``` 93 | 94 | ### Testing 95 | 96 | Run basic connection tests 97 | 98 | ```bash 99 | $ make test 100 | ``` 101 | 102 | ### Configuration 103 | 104 | ## Recommendations 105 | 106 | * Do not allow `root` (or `sudo`) access to this container as doing so would allow remote users to manipulate audit-logs in `/var/log/sudo-io` 107 | * Use the bastion as a "jump host" for accessing other internal systems rather than installing a lot of unnecessary stuff, which increases the overall attack surface. 108 | * Sync the contents of `/var/log/sudo-io` to a remote, offsite location. If using S3, we recommend enabling bucket-versioning. 109 | * Use [`github-authorized-keys](https://github.com/cloudposse/github-authorized-keys/) to automatically provision users; or use the [Helm chart](https://github.com/cloudposse/charts/tree/master/incubator/bastion). 110 | * Bind-mount `/etc/passwd`, `/etc/shadow` and `/etc/group` into the container as *read-only* 111 | * Bind-mount `/home` into container; the bastion does not manage authorized keys 112 | 113 | #### Environment Variables 114 | 115 | The following tables lists the most relevant environment variables of the `bastion` image and their default values. 116 | 117 | ##### Duo Settings 118 | 119 | Duo is a enterprise MFA provider that is very affordable. Details here: https://duo.com/pricing 120 | 121 | 122 | | ENV | Description | Default | 123 | |-------------------|:----------------------------------------------------|:--------:| 124 | | `MFA_PROVIDER` | Enable the Duo MFA provider | duo | 125 | | `DUO_IKEY` | Duo Integration Key | | 126 | | `DUO_SKEY` | Duo Secret Key | | 127 | | `DUO_HOST` | Duo Host Endpoint | | 128 | | `DUO_FAILMODE` | How to fail if Duo cannot be reached | secure | 129 | | `DUO_AUTOPUSH` | Automatically send a push notification | yes | 130 | | `DUO_PROMPTS` | How many times to prompt for MFA | 1 | 131 | 132 | 133 | ##### Google Authenticator Settings 134 | 135 | Google Authenticator is a free & open source MFA solution. It's less secure than Duo because tokens are stored on the server under each user account. 136 | 137 | 138 | | ENV | Description | Default | 139 | |-------------------|:----------------------------------------------------|:---------------------:| 140 | | `MFA_PROVIDER` | Enable the Google Authenticator provider | google-authenticator | 141 | 142 | 143 | ##### Enforcer Settings 144 | 145 | The enforcer ensures certain conditions are satisfied. Currently, these options are supported. 146 | 147 | | ENV | Description | Default | 148 | |-------------------------------|:-------------------------------------------------------------|:--------:| 149 | | `ENFORCER_ENABLED` | Enable general enforcement | `true` | 150 | | `ENFORCER_CLEAN_HOME_ENABLED` | Erase dot files in home directory before starting session | `true` | 151 | 152 | ##### Slack Notifications 153 | 154 | The enforcer is able to send notifications to a slack channel anytime there is an SSH login. 155 | 156 | | ENV | Description | Default | 157 | |----------------------------|:----------------------------------------------------|:---------:| 158 | | `SLACK_ENABLED` | Enabled Slack integration | `false` | 159 | | `SLACK_HOOK` | Slack integration method (e.g. `pam`, `sshrc`) | `sshrc` | 160 | | `SLACK_WEBHOOK_URL` | Webhook URL | | 161 | | `SLACK_USERNAME` | Slack handle of bot (defaults to short-dns name) | | 162 | | `SLACK_TIMEOUT` | Request timeout | `2` | 163 | | `SLACK_FATAL_ERRORS` | Deny logins if slack notification fails | `true` | 164 | 165 | 166 | ##### SSH Auditor 167 | 168 | The SSH auditor uses [`sudosh`](https://github.com/cloudposse/sudosh/) to record entire SSH sessions (`stdin`, `stdout`, and `stderr`). 169 | 170 | 171 | | ENV | Description | Default | 172 | |-----------------------|:----------------------------------------------------|:------------:| 173 | | `SSH_AUDIT_ENABLED` | Enable the SSH Audit facility | `true` | 174 | 175 | This will require that users login with the `/usr/bin/sudosh` shell. 176 | 177 | Update user's default shell by running the command: `usermod -s /usr/bin/sudosh $username`. By default, `root` will automatically be updated to use `sudosh`. 178 | 179 | Use the `sudoreplay` command to audit/replay sessions. 180 | 181 | 182 | #### User Accounts & SSH Keys 183 | 184 | The `bastion` does not attempt to manage user accounts. We suggest using [GitHub Authorized Keys](https://github.com/cloudposse/github-authorized-keys) to provision user accounts and SSH keys. We provide a [chart](https://github.com/cloudposse/charts/tree/master/incubator/bastion) of how we recommend doing it. 185 | 186 | ### Extending 187 | 188 | The `bastion` was written to be easily extensible. 189 | 190 | You can extend the enforcement policies by adding shell scripts to `etc/enforce.d`. Any scripts that are `+x` (e.g. `chmod 755`) will be executed at runtime. 191 | 192 | ## Thanks 193 | 194 | - [@neochrome](https://github.com/neochrome/docker-bastion), for providing a great basic bastion built on top of Alpine Linux 195 | - [@aws](https://aws.amazon.com/blogs/security/how-to-record-ssh-sessions-established-through-a-bastion-host/), for providing detailed instructions on how to do SSH session logging. 196 | - [@duo](https://duo.com/docs/duounix), for providing excellent documentation 197 | - [@google](https://github.com/google/google-authenticator-libpam) for contributing Google Authenticator to the Open Source community 198 | 199 | # Contributors to this project 200 | contributors: 201 | - name: "Erik Osterman" 202 | github: "osterman" 203 | - name: "Marji Cermak" 204 | github: "marji" 205 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2016 Cloud Posse, LLC 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /examples/cloudformation/cloudformation.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Description: Bastion 3 | 4 | Parameters: 5 | 6 | KeyPairName: 7 | Description: >- 8 | Enter a Public/private key pair. If you do not have one in this region, 9 | please create it before continuing 10 | Type: 'AWS::EC2::KeyPair::KeyName' 11 | NumBastionHosts: 12 | AllowedValues: 13 | - '1' 14 | - '2' 15 | - '3' 16 | - '4' 17 | Default: '1' 18 | Description: Enter the number of bastion hosts to create 19 | Type: String 20 | NetworkStackName: 21 | Description: Name of an active CloudFormation stack of networking resources 22 | Type: String 23 | MinLength: 1 24 | MaxLength: 255 25 | AllowedPattern: "^[a-zA-Z][-a-zA-Z0-9]*$" 26 | 27 | Resources: 28 | 29 | BastionAutoScalingGroup: 30 | Type: 'AWS::AutoScaling::AutoScalingGroup' 31 | Properties: 32 | LaunchConfigurationName: !Ref BastionLaunchConfiguration 33 | VPCZoneIdentifier: 34 | - !ImportValue 35 | Fn::Sub: "${NetworkStackName}-PublicSubnet1ID" 36 | - !ImportValue 37 | Fn::Sub: "${NetworkStackName}-PublicSubnet2ID" 38 | - !ImportValue 39 | Fn::Sub: "${NetworkStackName}-PublicSubnet3ID" 40 | MinSize: 0 41 | MaxSize: 3 42 | Cooldown: '300' 43 | DesiredCapacity: !Ref NumBastionHosts 44 | Tags: 45 | - Key: Name 46 | Value: !Sub ${AWS::StackName} 47 | PropagateAtLaunch: 'true' 48 | CreationPolicy: 49 | ResourceSignal: 50 | Count: !Ref NumBastionHosts 51 | Timeout: PT30M 52 | 53 | BastionECSCluster: 54 | Type: AWS::ECS::Cluster 55 | Properties: 56 | ClusterName: !Sub ${AWS::StackName} 57 | 58 | BastionEC2Role: 59 | Type: AWS::IAM::Role 60 | Properties: 61 | AssumeRolePolicyDocument: 62 | Statement: 63 | - Effect: Allow 64 | Principal: 65 | Service: [ec2.amazonaws.com] 66 | Action: ['sts:AssumeRole'] 67 | Path: / 68 | Policies: 69 | - PolicyName: ecs-service 70 | PolicyDocument: 71 | Statement: 72 | - Effect: Allow 73 | Action: [ 74 | 'ecs:CreateCluster', 75 | 'ecs:DeregisterContainerInstance', 76 | 'ecs:DiscoverPollEndpoint', 77 | 'ecs:Poll', 78 | 'ecs:RegisterContainerInstance', 79 | 'ecs:StartTelemetrySession', 80 | 'ecs:Submit*', 81 | "ecr:GetAuthorizationToken", 82 | "ecr:BatchCheckLayerAvailability", 83 | "ecr:GetDownloadUrlForLayer", 84 | "ecr:BatchGetImage", 85 | 'logs:CreateLogStream', 86 | 'logs:PutLogEvents' 87 | ] 88 | Resource: '*' 89 | 90 | BastionEC2InstanceProfile: 91 | Type: AWS::IAM::InstanceProfile 92 | Properties: 93 | Path: / 94 | Roles: [!Ref 'BastionEC2Role'] 95 | 96 | BastionLaunchConfiguration: 97 | Type: AWS::AutoScaling::LaunchConfiguration 98 | Metadata: 99 | AWS::CloudFormation::Init: 100 | config: 101 | files: 102 | /usr/bin/github-authorized-keys: 103 | content: | 104 | #!/bin/sh 105 | set -ue 106 | API_URL="${API_URL:-http://github-authorized-keys:301/user/%s/authorized_keys}" 107 | if [ -n "$1" ]; then 108 | exec curl --silent --fail $(printf "$API_URL" "$1") 109 | else 110 | echo "Usage: $0 [github username]" 111 | fi 112 | mode: '000550' 113 | owner: root 114 | group: root 115 | /etc/ssh/cloudposse_sshd_config: 116 | content: | 117 | Port 22 118 | AddressFamily any 119 | ListenAddress 0.0.0.0 120 | ListenAddress :: 121 | 122 | Protocol 2 123 | HostKey /etc/ssh/ssh_host_rsa_key 124 | 125 | # Lifetime and size of ephemeral version 1 server key 126 | #KeyRegenerationInterval 1h 127 | #ServerKeyBits 1024 128 | 129 | # Ciphers and keying 130 | #RekeyLimit default none 131 | 132 | # Logging 133 | # obsoletes QuietMode and FascistLogging 134 | #SyslogFacility AUTH 135 | #LogLevel INFO 136 | 137 | # Authentication: 138 | 139 | LoginGraceTime 2m 140 | PermitRootLogin yes 141 | PermitUserRC no 142 | StrictModes no 143 | MaxAuthTries 6 144 | MaxSessions 10 145 | 146 | PubkeyAuthentication yes 147 | 148 | # The default is to check both .ssh/authorized_keys and .ssh/authorized_keys2 149 | # but this is overridden so installations will only check .ssh/authorized_keys 150 | AuthorizedKeysFile .ssh/authorized_keys 151 | 152 | #AuthorizedPrincipalsFile none 153 | 154 | # Don't read the user's ~/.rhosts and ~/.shosts files 155 | IgnoreRhosts yes 156 | 157 | # To disable tunneled clear text passwords, change to no here! 158 | #PasswordAuthentication yes 159 | PermitEmptyPasswords no 160 | 161 | # Change to no to disable s/key passwords 162 | ChallengeResponseAuthentication yes 163 | 164 | UsePAM yes 165 | AuthenticationMethods publickey,keyboard-interactive 166 | AllowAgentForwarding yes 167 | AllowTcpForwarding no 168 | GatewayPorts no 169 | X11Forwarding no 170 | PermitTTY yes 171 | PrintMotd no 172 | PrintLastLog yes 173 | TCPKeepAlive yes 174 | #UseLogin no 175 | UsePrivilegeSeparation sandbox 176 | PermitUserEnvironment no 177 | #Compression delayed 178 | ClientAliveInterval 30 179 | ClientAliveCountMax 3 180 | UseDNS no 181 | #PidFile /run/sshd.pid 182 | PermitTunnel yes 183 | ChrootDirectory none 184 | VersionAddendum none 185 | 186 | # no default banner path 187 | Banner none 188 | 189 | # override default of no subsystems 190 | Subsystem sftp /usr/lib/ssh/sftp-server -l VERBOSE 191 | 192 | # the following are HPN related configuration options 193 | # tcp receive buffer polling. disable in non autotuning kernels 194 | #TcpRcvBufPoll yes 195 | 196 | # disable hpn performance boosts 197 | #HPNDisabled no 198 | 199 | # buffer size for hpn to non-hpn connections 200 | #HPNBufferSize 2048 201 | 202 | ForceCommand /usr/bin/fc 203 | 204 | # Example of overriding settings on a per-user basis 205 | #Match User anoncvs 206 | # X11Forwarding no 207 | # AllowTcpForwarding no 208 | # PermitTTY no 209 | # ForceCommand cvs server 210 | 211 | AuthorizedKeysCommand /usr/bin/github-authorized-keys 212 | AuthorizedKeysCommandUser root 213 | Properties: 214 | ImageId: ami-0092e55c70015d8c3 # ECS AMI 215 | InstanceType: t2.micro 216 | IamInstanceProfile: 217 | Ref: BastionEC2InstanceProfile 218 | KeyName: 219 | Ref: KeyPairName 220 | SecurityGroups: 221 | - !ImportValue 222 | Fn::Sub: "${NetworkStackName}-BastionSecurityGroupID" 223 | AssociatePublicIpAddress: true 224 | UserData: 225 | Fn::Base64: !Sub | 226 | #!/bin/bash -xe 227 | 228 | yum install -y aws-cfn-bootstrap 229 | 230 | echo ECS_CLUSTER=${AWS::StackName} >> /etc/ecs/ecs.config 231 | 232 | # Process the default configset from the CloudFormation::Init metadata 233 | /opt/aws/bin/cfn-init -v \ 234 | --region ${AWS::Region} \ 235 | --stack ${AWS::StackName} \ 236 | --resource BastionLaunchConfiguration \ 237 | --configsets default 238 | 239 | # Signal BastionAutoScalingGroup with the cfn-init exit status 240 | /opt/aws/bin/cfn-signal -e $? \ 241 | --region ${AWS::Region} \ 242 | --stack ${AWS::StackName} \ 243 | --resource BastionAutoScalingGroup 244 | 245 | TaskDefinition: 246 | Type: AWS::ECS::TaskDefinition 247 | Properties: 248 | ContainerDefinitions: 249 | - Name: 'github-authorized-keys' 250 | MountPoints: 251 | - SourceVolume: "host" 252 | ContainerPath: "/host" 253 | Image: "cloudposse/github-authorized-keys" 254 | Cpu: "100" 255 | Memory: "64" 256 | Essential: "true" 257 | Environment: 258 | - Name: GITHUB_API_TOKEN 259 | Value: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 260 | - Name: GITHUB_ORGANIZATION 261 | Value: xxxxxx 262 | - Name: GITHUB_TEAM 263 | Value: xxxxx 264 | - Name: SYNC_USERS_SHELL 265 | Value: /bin/bash 266 | - Name: SYNC_USERS_ROOT 267 | Value: /host 268 | - Name: SYNC_USERS_INTERVAL 269 | Value: 300 270 | - Name: LISTEN 271 | Value: :301 272 | - Name: INTEGRATE_SSH 273 | Value: 'false' 274 | - Name: LINUX_USER_ADD_TPL 275 | Value: 'adduser -s {shell} {username}' 276 | - Name: LINUX_USER_ADD_WITH_GID_TPL 277 | Value: 'adduser -s {shell} -G {group} {username}' 278 | PortMappings: 279 | - ContainerPort: 301 280 | HostPort: 301 281 | Protocol: tcp 282 | 283 | - Name: 'bastion' 284 | MountPoints: 285 | - SourceVolume: "root" 286 | ContainerPath: "/root" 287 | - SourceVolume: "home" 288 | ContainerPath: "/home" 289 | - SourceVolume: "etc-shadow" 290 | ContainerPath: "/etc/shadow" 291 | - SourceVolume: "etc-passwd" 292 | ContainerPath: "/etc/passwd" 293 | - SourceVolume: "etc-group" 294 | ContainerPath: "/etc/group" 295 | - SourceVolume: "sshd_config" 296 | ContainerPath: "/etc/ssh/sshd_config" 297 | - SourceVolume: "usr-bin-github-authorized-keys" 298 | ContainerPath: "/usr/bin/github-authorized-keys" 299 | Image: "cloudposse/bastion" 300 | Cpu: "100" 301 | Memory: "128" 302 | Essential: "true" 303 | Links: 304 | - github-authorized-keys 305 | Environment: 306 | - Name: DUO_IKEY 307 | Value: xxxxxxxxxxxxxxxxxxx 308 | - Name: DUO_SKEY 309 | Value: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 310 | - Name: DUO_HOST 311 | Value: api-xxxxx.duosecurity.com 312 | - Name: SSH_AUDIT_ENABLED 313 | Value: 'false' 314 | PortMappings: 315 | - ContainerPort: 22 316 | HostPort: 1234 317 | Protocol: tcp 318 | Volumes: 319 | - Name: "host" 320 | Host: 321 | SourcePath: "/" 322 | - Name: "root" 323 | Host: 324 | SourcePath: "/root" 325 | - Name: "home" 326 | Host: 327 | SourcePath: "/home" 328 | - Name: "etc-shadow" 329 | Host: 330 | SourcePath: "/etc/shadow" 331 | - Name: "etc-passwd" 332 | Host: 333 | SourcePath: "/etc/passwd" 334 | - Name: "etc-group" 335 | Host: 336 | SourcePath: "/etc/group" 337 | - Name: "sshd_config" 338 | Host: 339 | SourcePath: "/etc/ssh/cloudposse_sshd_config" 340 | - Name: "usr-bin-github-authorized-keys" 341 | Host: 342 | SourcePath: "/usr/bin/github-authorized-keys" 343 | 344 | 345 | ECSService: 346 | Type: AWS::ECS::Service 347 | Properties: 348 | Cluster: !Sub ${AWS::StackName} 349 | DesiredCount: !Ref NumBastionHosts 350 | TaskDefinition: !Ref TaskDefinition 351 | DeploymentConfiguration: 352 | MinimumHealthyPercent: 0 353 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Project Banner
5 |

6 | Latest ReleaseBuild & Test StatusSlack Community

7 | 8 | 9 | 29 | 30 | This is a secure/locked-down bastion implemented as a Docker Container. It uses Alpine Linux as the base image and ships with support for Google Authenticator & DUO MFA support. 31 | 32 | It was designed to be used on Kubernetes together with [GitHub Authorized Keys](https://github.com/cloudposse/github-authorized-keys) to provide secure remote access to production clusters. 33 | ### MFA Setup & Usage 34 | 35 | Here's a demo of what a user experiences when setting up Google Authenticator for the first time. 36 | 37 | ![Demo 1](docs/demo.gif) 38 | 39 | When using Duo as the MFA provider, this becomes even more magical because Duo supports automatic Push notifications to your mobile device. 40 | Just approve the request on your mobile phone (e.g. with a thumb press on iOS) when prompted. 41 | 42 | ### Slack Notifications 43 | 44 | Here's what it looks like when someone connects to the bastion if Slack notifications are enabled. 45 | 46 | ![Demo 2](docs/slack.png) 47 | 48 | We recommend using Slack notifications for self-reporting. 49 | * Any time a user accesses production systems, they should reply to the slack notification to justify their remote access. 50 | * A "buddy" should approve the login by adding a reaction (e.g. ✅). 51 | * If no one approves the login, it should trigger an *incident response* to track down the unauthorized access. 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | ## Usage 60 | 61 | ### Running 62 | 63 | Refer to the [Environment Variables](#environment-variables) section below to tune how the `bastion` operates. 64 | 65 | 66 | ```bash 67 | $ docker run -p 1234:22 cloudposse/bastion:latest 68 | ``` 69 | 70 | ### Building 71 | 72 | ```bash 73 | $ git clone https://github.com/cloudposse/bastion.git 74 | $ cd bastion 75 | $ make docker:build 76 | ``` 77 | 78 | ### Testing 79 | 80 | Run basic connection tests 81 | 82 | ```bash 83 | $ make test 84 | ``` 85 | 86 | ### Configuration 87 | 88 | ## Recommendations 89 | 90 | * Do not allow `root` (or `sudo`) access to this container as doing so would allow remote users to manipulate audit-logs in `/var/log/sudo-io` 91 | * Use the bastion as a "jump host" for accessing other internal systems rather than installing a lot of unnecessary stuff, which increases the overall attack surface. 92 | * Sync the contents of `/var/log/sudo-io` to a remote, offsite location. If using S3, we recommend enabling bucket-versioning. 93 | * Use [`github-authorized-keys](https://github.com/cloudposse/github-authorized-keys/) to automatically provision users; or use the [Helm chart](https://github.com/cloudposse/charts/tree/master/incubator/bastion). 94 | * Bind-mount `/etc/passwd`, `/etc/shadow` and `/etc/group` into the container as *read-only* 95 | * Bind-mount `/home` into container; the bastion does not manage authorized keys 96 | 97 | #### Environment Variables 98 | 99 | The following tables lists the most relevant environment variables of the `bastion` image and their default values. 100 | 101 | ##### Duo Settings 102 | 103 | Duo is a enterprise MFA provider that is very affordable. Details here: https://duo.com/pricing 104 | 105 | 106 | | ENV | Description | Default | 107 | |-------------------|:----------------------------------------------------|:--------:| 108 | | `MFA_PROVIDER` | Enable the Duo MFA provider | duo | 109 | | `DUO_IKEY` | Duo Integration Key | | 110 | | `DUO_SKEY` | Duo Secret Key | | 111 | | `DUO_HOST` | Duo Host Endpoint | | 112 | | `DUO_FAILMODE` | How to fail if Duo cannot be reached | secure | 113 | | `DUO_AUTOPUSH` | Automatically send a push notification | yes | 114 | | `DUO_PROMPTS` | How many times to prompt for MFA | 1 | 115 | 116 | 117 | ##### Google Authenticator Settings 118 | 119 | Google Authenticator is a free & open source MFA solution. It's less secure than Duo because tokens are stored on the server under each user account. 120 | 121 | 122 | | ENV | Description | Default | 123 | |-------------------|:----------------------------------------------------|:---------------------:| 124 | | `MFA_PROVIDER` | Enable the Google Authenticator provider | google-authenticator | 125 | 126 | 127 | ##### Enforcer Settings 128 | 129 | The enforcer ensures certain conditions are satisfied. Currently, these options are supported. 130 | 131 | | ENV | Description | Default | 132 | |-------------------------------|:-------------------------------------------------------------|:--------:| 133 | | `ENFORCER_ENABLED` | Enable general enforcement | `true` | 134 | | `ENFORCER_CLEAN_HOME_ENABLED` | Erase dot files in home directory before starting session | `true` | 135 | 136 | ##### Slack Notifications 137 | 138 | The enforcer is able to send notifications to a slack channel anytime there is an SSH login. 139 | 140 | | ENV | Description | Default | 141 | |----------------------------|:----------------------------------------------------|:---------:| 142 | | `SLACK_ENABLED` | Enabled Slack integration | `false` | 143 | | `SLACK_HOOK` | Slack integration method (e.g. `pam`, `sshrc`) | `sshrc` | 144 | | `SLACK_WEBHOOK_URL` | Webhook URL | | 145 | | `SLACK_USERNAME` | Slack handle of bot (defaults to short-dns name) | | 146 | | `SLACK_TIMEOUT` | Request timeout | `2` | 147 | | `SLACK_FATAL_ERRORS` | Deny logins if slack notification fails | `true` | 148 | 149 | 150 | ##### SSH Auditor 151 | 152 | The SSH auditor uses [`sudosh`](https://github.com/cloudposse/sudosh/) to record entire SSH sessions (`stdin`, `stdout`, and `stderr`). 153 | 154 | 155 | | ENV | Description | Default | 156 | |-----------------------|:----------------------------------------------------|:------------:| 157 | | `SSH_AUDIT_ENABLED` | Enable the SSH Audit facility | `true` | 158 | 159 | This will require that users login with the `/usr/bin/sudosh` shell. 160 | 161 | Update user's default shell by running the command: `usermod -s /usr/bin/sudosh $username`. By default, `root` will automatically be updated to use `sudosh`. 162 | 163 | Use the `sudoreplay` command to audit/replay sessions. 164 | 165 | 166 | #### User Accounts & SSH Keys 167 | 168 | The `bastion` does not attempt to manage user accounts. We suggest using [GitHub Authorized Keys](https://github.com/cloudposse/github-authorized-keys) to provision user accounts and SSH keys. We provide a [chart](https://github.com/cloudposse/charts/tree/master/incubator/bastion) of how we recommend doing it. 169 | 170 | ### Extending 171 | 172 | The `bastion` was written to be easily extensible. 173 | 174 | You can extend the enforcement policies by adding shell scripts to `etc/enforce.d`. Any scripts that are `+x` (e.g. `chmod 755`) will be executed at runtime. 175 | 176 | ## Thanks 177 | 178 | - [@neochrome](https://github.com/neochrome/docker-bastion), for providing a great basic bastion built on top of Alpine Linux 179 | - [@aws](https://aws.amazon.com/blogs/security/how-to-record-ssh-sessions-established-through-a-bastion-host/), for providing detailed instructions on how to do SSH session logging. 180 | - [@duo](https://duo.com/docs/duounix), for providing excellent documentation 181 | - [@google](https://github.com/google/google-authenticator-libpam) for contributing Google Authenticator to the Open Source community 182 | 183 | 184 | ## Quick Start 185 | 186 | 187 | Here's how you can quickly demo the `bastion`. We assume you have `~/.ssh/authorized_keys` properly configured and your SSH key (e.g. `~/.ssh/id_rsa`) added to your SSH agent. 188 | 189 | 190 | ```bash 191 | $ docker run -it -p 1234:22 \ 192 | -e MFA_PROVIDER=google-authenticator \ 193 | -v ~/.ssh/authorized_keys:/root/.ssh/authorized_keys \ 194 | cloudposse/bastion 195 | ``` 196 | 197 | Now, in another terminal you should be able to run: 198 | ```bash 199 | $ ssh root@localhost -p 1234 200 | ``` 201 | 202 | The first time you connect, you'll be asked to setup your MFA device. Subsequently, each time you connect, you'll be prompted to enter your MFA token. 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | ## ✨ Contributing 211 | 212 | This project is under active development, and we encourage contributions from our community. 213 | 214 | 215 | 216 | Many thanks to our outstanding contributors: 217 | 218 | 219 | 220 | 221 | 222 | For 🐛 bug reports & feature requests, please use the [issue tracker](https://github.com/cloudposse/bastion/issues). 223 | 224 | In general, PRs are welcome. We follow the typical "fork-and-pull" Git workflow. 225 | 1. Review our [Code of Conduct](https://github.com/cloudposse/bastion/?tab=coc-ov-file#code-of-conduct) and [Contributor Guidelines](https://github.com/cloudposse/.github/blob/main/CONTRIBUTING.md). 226 | 2. **Fork** the repo on GitHub 227 | 3. **Clone** the project to your own machine 228 | 4. **Commit** changes to your own branch 229 | 5. **Push** your work back up to your fork 230 | 6. Submit a **Pull Request** so that we can review your changes 231 | 232 | **NOTE:** Be sure to merge the latest changes from "upstream" before making a pull request! 233 | 234 | ### 🌎 Slack Community 235 | 236 | Join our [Open Source Community](https://cpco.io/slack?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/bastion&utm_content=slack) on Slack. It's **FREE** for everyone! Our "SweetOps" community is where you get to talk with others who share a similar vision for how to rollout and manage infrastructure. This is the best place to talk shop, ask questions, solicit feedback, and work together as a community to build totally *sweet* infrastructure. 237 | 238 | ### 📰 Newsletter 239 | 240 | Sign up for [our newsletter](https://cpco.io/newsletter?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/bastion&utm_content=newsletter) and join 3,000+ DevOps engineers, CTOs, and founders who get insider access to the latest DevOps trends, so you can always stay in the know. 241 | Dropped straight into your Inbox every week — and usually a 5-minute read. 242 | 243 | ### 📆 Office Hours 244 | 245 | [Join us every Wednesday via Zoom](https://cloudposse.com/office-hours?utm_source=github&utm_medium=readme&utm_campaign=cloudposse/bastion&utm_content=office_hours) for your weekly dose of insider DevOps trends, AWS news and Terraform insights, all sourced from our SweetOps community, plus a _live Q&A_ that you can’t find anywhere else. 246 | It's **FREE** for everyone! 247 | ## License 248 | 249 | License 250 | 251 |
252 | Preamble to the Apache License, Version 2.0 253 |
254 |
255 | 256 | Complete license is available in the [`LICENSE`](LICENSE) file. 257 | 258 | ```text 259 | Licensed to the Apache Software Foundation (ASF) under one 260 | or more contributor license agreements. See the NOTICE file 261 | distributed with this work for additional information 262 | regarding copyright ownership. The ASF licenses this file 263 | to you under the Apache License, Version 2.0 (the 264 | "License"); you may not use this file except in compliance 265 | with the License. You may obtain a copy of the License at 266 | 267 | https://www.apache.org/licenses/LICENSE-2.0 268 | 269 | Unless required by applicable law or agreed to in writing, 270 | software distributed under the License is distributed on an 271 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 272 | KIND, either express or implied. See the License for the 273 | specific language governing permissions and limitations 274 | under the License. 275 | ``` 276 |
277 | 278 | ## Trademarks 279 | 280 | All other trademarks referenced herein are the property of their respective owners. 281 | 282 | 283 | --- 284 | Copyright © 2017-2025 [Cloud Posse, LLC](https://cpco.io/copyright) 285 | 286 | 287 | README footer 288 | 289 | Beacon 290 | --------------------------------------------------------------------------------