├── test ├── .gitignore ├── src │ └── test │ │ └── java │ │ └── de │ │ └── widdix │ │ └── awsec2ssh │ │ ├── Config.java │ │ ├── ATest.java │ │ ├── AAWSTest.java │ │ ├── TestShowcase.java │ │ ├── ACloudFormationTest.java │ │ └── TestShowcaseRPM.java ├── README.md └── pom.xml ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE.md ├── docs └── architecture.png ├── aws-ec2-ssh.conf ├── iam_crossaccount_policy.json ├── iam_ssh_policy.json ├── install_configure_sshd.sh ├── install_configure_selinux.sh ├── LICENSE ├── install_restart_sshd.sh ├── authorized_keys_command.sh ├── DEV.md ├── aws-ec2-ssh.spec ├── install.sh ├── README.md ├── import_users.sh ├── showcase-rpm.yaml └── showcase.yaml /test/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .idea/ 3 | *.iml 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: widdix 4 | -------------------------------------------------------------------------------- /docs/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/widdix/aws-ec2-ssh/HEAD/docs/architecture.png -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Metadata: 2 | * Operating System: (enter value here) 3 | * Installation method: (choose between RPM or install.sh script) 4 | * AWS CLI Version: (exec `aws --version` and enter value here) 5 | 6 | (describe your issue here) 7 | -------------------------------------------------------------------------------- /aws-ec2-ssh.conf: -------------------------------------------------------------------------------- 1 | IAM_AUTHORIZED_GROUPS="" 2 | LOCAL_MARKER_GROUP="iam-synced-users" 3 | LOCAL_GROUPS="" 4 | SUDOERS_GROUPS="" 5 | ASSUMEROLE="" 6 | 7 | # Remove or set to 0 if you are done with configuration 8 | # To change the interval of the sync change the file 9 | # /etc/cron.d/import_users 10 | DONOTSYNC=0 11 | -------------------------------------------------------------------------------- /iam_crossaccount_policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [{ 4 | "Effect": "Allow", 5 | "Action": "sts:AssumeRole", 6 | "Resource": [ 7 | "arn:aws:iam:::role/" 8 | ] 9 | },{ 10 | "Effect": "Allow", 11 | "Action": "ec2:DescribeTags", 12 | "Resource": "*" 13 | }] 14 | } 15 | -------------------------------------------------------------------------------- /iam_ssh_policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [{ 4 | "Effect": "Allow", 5 | "Action": [ 6 | "iam:ListUsers", 7 | "iam:GetGroup" 8 | ], 9 | "Resource": "*" 10 | }, { 11 | "Effect": "Allow", 12 | "Action": [ 13 | "iam:GetSSHPublicKey", 14 | "iam:ListSSHPublicKeys" 15 | ], 16 | "Resource": [ 17 | "arn:aws:iam:::user/*" 18 | ] 19 | }, { 20 | "Effect": "Allow", 21 | "Action": "ec2:DescribeTags", 22 | "Resource": "*" 23 | }] 24 | } 25 | -------------------------------------------------------------------------------- /install_configure_sshd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # add new line if file does not end with new line 4 | if [ -n "$(tail -c1 ${SSHD_CONFIG_FILE})" ]; then 5 | echo >> ${SSHD_CONFIG_FILE} 6 | fi 7 | 8 | if ! grep -q '^AuthorizedKeysCommand /opt/authorized_keys_command.sh' ${SSHD_CONFIG_FILE}; then 9 | sed -e '/AuthorizedKeysCommand / s/^#*/#/' -i ${SSHD_CONFIG_FILE}; echo "AuthorizedKeysCommand ${AUTHORIZED_KEYS_COMMAND_FILE}" >> ${SSHD_CONFIG_FILE} 10 | fi 11 | 12 | if ! grep -q '^AuthorizedKeysCommandUser nobody' ${SSHD_CONFIG_FILE}; then 13 | sed -e '/AuthorizedKeysCommandUser / s/^#*/#/' -i ${SSHD_CONFIG_FILE}; echo 'AuthorizedKeysCommandUser nobody' >> ${SSHD_CONFIG_FILE} 14 | fi 15 | -------------------------------------------------------------------------------- /install_configure_selinux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # In order to support SELinux in Enforcing mode, we need to tell SELinux that it 4 | # should have the nis_enabled boolean turned on (so it should expect login services 5 | # like PAM and sshd to make calls to get public keys from a remote server) 6 | # 7 | # This is observed on CentOS 7 and RHEL 7 8 | 9 | # Capture the return code and use that to determine if we have the command available 10 | retval=0 11 | which getenforce > /dev/null 2>&1 || retval=$? 12 | 13 | if [[ "$retval" -eq "0" ]]; then 14 | retval=0 15 | selinuxenabled || retval=$? 16 | if [[ "$retval" -eq "0" ]]; then 17 | setsebool -P nis_enabled on 18 | fi 19 | fi 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 widdix GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /install_restart_sshd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # Restart sshd using an appropriate method based on the currently running init daemon 4 | # Note that systemd can return "running" or "degraded" (If a systemd unit has failed) 5 | # This was observed on the RHEL 7.3 AMI, so it's added for completeness 6 | # systemd is also not standardized in the name of the ssh service, nor in the places 7 | # where the unit files are stored. 8 | 9 | # Capture the return code and use that to determine if we have the command available 10 | retval=0 11 | which systemctl > /dev/null 2>&1 || retval=$? 12 | 13 | if [[ "$retval" -eq "0" ]]; then 14 | if [[ ($(systemctl is-system-running) =~ running) || ($(systemctl is-system-running) =~ degraded) || ($(systemctl is-system-running) =~ starting) ]]; then 15 | if [ -f "/usr/lib/systemd/system/sshd.service" ] || [ -f "/lib/systemd/system/sshd.service" ]; then 16 | systemctl restart sshd.service 17 | else 18 | systemctl restart ssh.service 19 | fi 20 | fi 21 | elif [[ $(/sbin/init --version) =~ upstart ]]; then 22 | if [ -f "/etc/init.d/sshd" ]; then 23 | service sshd restart 24 | else 25 | service ssh restart 26 | fi 27 | else 28 | if [ -f "/etc/init.d/sshd" ]; then 29 | /etc/init.d/sshd restart 30 | else 31 | /etc/init.d/ssh restart 32 | fi 33 | fi 34 | -------------------------------------------------------------------------------- /test/src/test/java/de/widdix/awsec2ssh/Config.java: -------------------------------------------------------------------------------- 1 | package de.widdix.awsec2ssh; 2 | 3 | public final class Config { 4 | 5 | public enum Key { 6 | TEMPLATE_DIR("TEMPLATE_DIR"), 7 | IAM_ROLE_ARN("IAM_ROLE_ARN"), 8 | DELETION_POLICY("DELETION_POLICY", "delete"), 9 | VERSION("VERSION"); 10 | 11 | private final String name; 12 | private final String defaultValue; 13 | 14 | Key(String name, String defaultValue) { 15 | this.name = name; 16 | this.defaultValue = defaultValue; 17 | } 18 | 19 | Key(String name) { 20 | this.name = name; 21 | this.defaultValue = null; 22 | } 23 | } 24 | 25 | public static String get(final Key key) { 26 | final String env = System.getenv(key.name); 27 | if (env == null) { 28 | if (key.defaultValue == null) { 29 | throw new RuntimeException("config not found: " + key.name); 30 | } else { 31 | return key.defaultValue; 32 | } 33 | } else { 34 | return env; 35 | } 36 | } 37 | 38 | public static boolean has(final Key key) { 39 | final String env = System.getenv(key.name); 40 | if (env == null) { 41 | if (key.defaultValue == null) { 42 | return false; 43 | } else { 44 | return true; 45 | } 46 | } else { 47 | return true; 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Showcase Test 2 | 3 | The goal of this tests is to ensure that the showcase is always working. The tests are implemented in Java 8 and run in JUnit 4. 4 | 5 | If you run this tests, an AWS CloudFormation stack is created and **charges may apply**! 6 | 7 | [widdix GmbH](https://widdix.net) sponsors the test runs on every push and once per week to ensure that everything is working as expected. 8 | 9 | ## Supported env variables 10 | 11 | * `IAM_ROLE_ARN` if the tests should assume an IAM role before they run supply the ARN of the IAM role 12 | * `TEMPLATE_DIR` Load templates from local disk. Must end with an `/`. 13 | * `DELETION_POLICY` (default `delete`, allowed values [`delete`, `retain`]) should resources be deleted? 14 | * `VERSION` RPM version to test 15 | 16 | ## Usage 17 | 18 | ### AWS credentials 19 | 20 | The AWS credentials are passed in as defined by the AWS SDK for Java: http://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html 21 | 22 | One addition: you can supply the env variable `IAM_ROLE_ARN` which let's the tests assume a role with the default credentials before running the tests. 23 | 24 | ### Region selection 25 | 26 | The region selection works like defined by the AWS SDK for Java: http://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/java-dg-region-selection.html 27 | 28 | ### Run all tests 29 | 30 | ``` 31 | AWS_REGION="us-east-1" TEMPLATE_DIR="/path/to/widdix-aws-ec2-ssh/" mvn test 32 | ``` 33 | 34 | ### Assume role 35 | 36 | This is useful if you run on a integration server like Jenkins and want to assume a different IAM role for this tests. 37 | 38 | ``` 39 | IAM_ROLE_ARN="arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME" TEMPLATE_DIR="/path/to/widdix-aws-ec2-ssh/" mvn test mvn test 40 | ``` 41 | -------------------------------------------------------------------------------- /authorized_keys_command.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | if [ -z "$1" ]; then 4 | exit 1 5 | fi 6 | 7 | # check if AWS CLI exists 8 | if ! [ -x "$(which aws)" ]; then 9 | echo "aws executable not found - exiting!" 10 | exit 1 11 | fi 12 | 13 | # source configuration if it exists 14 | [ -f /etc/aws-ec2-ssh.conf ] && . /etc/aws-ec2-ssh.conf 15 | 16 | # Assume a role before contacting AWS IAM to get users and keys. 17 | # This can be used if you define your users in one AWS account, while the EC2 18 | # instance you use this script runs in another. 19 | : ${ASSUMEROLE:=""} 20 | 21 | if [[ ! -z "${ASSUMEROLE}" ]] 22 | then 23 | STSCredentials=$(aws sts assume-role \ 24 | --role-arn "${ASSUMEROLE}" \ 25 | --role-session-name something \ 26 | --query '[Credentials.SessionToken,Credentials.AccessKeyId,Credentials.SecretAccessKey]' \ 27 | --output text) 28 | 29 | AWS_ACCESS_KEY_ID=$(echo "${STSCredentials}" | awk '{print $2}') 30 | AWS_SECRET_ACCESS_KEY=$(echo "${STSCredentials}" | awk '{print $3}') 31 | AWS_SESSION_TOKEN=$(echo "${STSCredentials}" | awk '{print $1}') 32 | AWS_SECURITY_TOKEN=$(echo "${STSCredentials}" | awk '{print $1}') 33 | export AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_SECURITY_TOKEN 34 | fi 35 | 36 | UnsaveUserName="$1" 37 | UnsaveUserName=${UnsaveUserName//".plus."/"+"} 38 | UnsaveUserName=${UnsaveUserName//".equal."/"="} 39 | UnsaveUserName=${UnsaveUserName//".comma."/","} 40 | UnsaveUserName=${UnsaveUserName//".at."/"@"} 41 | 42 | aws iam list-ssh-public-keys --user-name "$UnsaveUserName" --query "SSHPublicKeys[?Status == 'Active'].[SSHPublicKeyId]" --output text | while read -r KeyId; do 43 | aws iam get-ssh-public-key --user-name "$UnsaveUserName" --ssh-public-key-id "$KeyId" --encoding SSH --query "SSHPublicKey.SSHPublicKeyBody" --output text 44 | done 45 | -------------------------------------------------------------------------------- /DEV.md: -------------------------------------------------------------------------------- 1 | # Developer notes 2 | 3 | ## Region Maps 4 | 5 | To update the region maps execute the following lines in your terminal: 6 | 7 | ### RegionMapAmazonLinux 8 | 9 | Default user: ec2-user 10 | 11 | ```bash 12 | for region in $(aws ec2 describe-regions --query "Regions[].RegionName" --output text); do ami=$(aws --region $region ec2 describe-images --filters "Name=name,Values=amzn-ami-hvm-2018.03.0.20211015.1-x86_64-gp2" --query "Images[0].ImageId" --output "text"); printf "'$region':\n AMI: '$ami'\n"; done 13 | ``` 14 | 15 | ### RegionMapAmazonLinux2 16 | 17 | Default user: ec2-user 18 | 19 | ```bash 20 | for region in $(aws ec2 describe-regions --query "Regions[].RegionName" --output text); do ami=$(aws --region $region ec2 describe-images --filters "Name=name,Values=amzn2-ami-hvm-2.0.20211005.0-x86_64-gp2" --query "Images[0].ImageId" --output "text"); printf "'$region':\n AMI: '$ami'\n"; done 21 | ``` 22 | 23 | ### RegionMapUbuntu 24 | 25 | Default user: ubuntu 26 | 27 | ```bash 28 | for region in $(aws ec2 describe-regions --query "Regions[].RegionName" --output text); do ami=$(aws --region $region ec2 describe-images --filters "Name=name,Values=ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64-server-20210928" --query "Images[0].ImageId" --output "text"); printf "'$region':\n AMI: '$ami'\n"; done 29 | ``` 30 | 31 | ### RegionMapSUSELinuxEnterpriseServer 32 | 33 | Default user: ec2-user 34 | 35 | ```bash 36 | for region in $(aws ec2 describe-regions --query "Regions[].RegionName" --output text); do ami=$(aws --region $region ec2 describe-images --filters "Name=name,Values=suse-sles-12-sp3-v20181004-hvm-ssd-x86_64" --query "Images[0].ImageId" --output "text"); printf "'$region':\n AMI: '$ami'\n"; done 37 | ``` 38 | 39 | ### RegionMapRHEL 40 | 41 | Default user: ec2-user 42 | 43 | ```bash 44 | for region in $(aws ec2 describe-regions --query "Regions[].RegionName" --output text); do ami=$(aws --region $region ec2 describe-images --filters "Name=name,Values=RHEL-7.4_HVM_GA-20170808-x86_64-2-Hourly2-GP2" --query "Images[0].ImageId" --output "text"); printf "'$region':\n AMI: '$ami'\n"; done 45 | ``` 46 | 47 | ### RegionMapCentOS 48 | 49 | Default user: centos 50 | 51 | ```bash 52 | for region in $(aws ec2 describe-regions --query "Regions[].RegionName" --output text); do ami=$(aws --region $region ec2 describe-images --filters "Name=name,Values=CentOS 7.9.2009 x86_64" --query "Images[0].ImageId" --output "text"); printf "'$region':\n AMI: '$ami'\n"; done 53 | ``` 54 | -------------------------------------------------------------------------------- /aws-ec2-ssh.spec: -------------------------------------------------------------------------------- 1 | %define name aws-ec2-ssh 2 | %define version %{jenkins_version} 3 | %define release %{jenkins_release}%{?dist} 4 | %define archive %{jenkins_archive} 5 | %define archivedir aws-ec2-ssh-%{jenkins_suffix} 6 | 7 | Name: %{name} 8 | Summary: Manage AWS EC2 SSH access with IAM 9 | Version: %{version} 10 | Release: %{release} 11 | 12 | Group: System/Administration 13 | License: MIT 14 | URL: https://cloudonaut.io/manage-aws-ec2-ssh-access-with-iam/ 15 | Source0: https://github.com/widdix/aws-ec2-ssh/archive/%{archive}.tar.gz 16 | BuildArch: noarch 17 | Vendor: widdix GmbH 18 | Packager: Michiel van Baak 19 | 20 | Requires: bash 21 | 22 | %description 23 | Use your IAM user's public SSH key to get access via SSH to an EC2 instance. 24 | 25 | 26 | %prep 27 | %setup -q -n %{archivedir} 28 | 29 | 30 | %build 31 | 32 | 33 | %install 34 | rm -rf ${RPM_BUILD_ROOT} 35 | mkdir -p ${RPM_BUILD_ROOT}%{_bindir} 36 | mkdir -p ${RPM_BUILD_ROOT}%{_sysconfdir}/cron.d 37 | install -m 755 import_users.sh ${RPM_BUILD_ROOT}%{_bindir} 38 | install -m 755 authorized_keys_command.sh ${RPM_BUILD_ROOT}%{_bindir} 39 | install -m 644 aws-ec2-ssh.conf ${RPM_BUILD_ROOT}%{_sysconfdir}/aws-ec2-ssh.conf 40 | sed -i 's/DONOTSYNC=0/DONOTSYNC=1/g' ${RPM_BUILD_ROOT}%{_sysconfdir}/aws-ec2-ssh.conf 41 | echo "*/10 * * * * root /usr/bin/import_users.sh" > ${RPM_BUILD_ROOT}%{_sysconfdir}/cron.d/import_users 42 | chmod 0644 ${RPM_BUILD_ROOT}%{_sysconfdir}/cron.d/import_users 43 | 44 | 45 | %post 46 | %include install_configure_sshd.sh 47 | %include install_configure_selinux.sh 48 | %include install_restart_sshd.sh 49 | echo "To configure the aws-ec2-ssh package, edit /etc/aws-ec2-ssh.conf. No users will be synchronized before you did this." 50 | 51 | 52 | %postun 53 | sed -i 's:AuthorizedKeysCommand /usr/bin/authorized_keys_command.sh:#AuthorizedKeysCommand none:g' /etc/ssh/sshd_config 54 | sed -i 's:AuthorizedKeysCommandUser nobody:#AuthorizedKeysCommandUser nobody:g' /etc/ssh/sshd_config 55 | %include install_restart_sshd.sh 56 | 57 | 58 | %clean 59 | rm -rf ${RPM_BUILD_ROOT} 60 | 61 | 62 | %files 63 | %defattr(-,root,root) 64 | %attr(755,root,root) %{_bindir}/import_users.sh 65 | %attr(755,root,root) %{_bindir}/authorized_keys_command.sh 66 | %config %{_sysconfdir}/aws-ec2-ssh.conf 67 | %config %{_sysconfdir}/cron.d/import_users 68 | 69 | 70 | %changelog 71 | 72 | * Wed May 3 2017 Michiel van Baak - 1.1.0-2 73 | - Create cron.d file and run import_users on install 74 | 75 | * Thu Apr 27 2017 Michiel van Baak - post-1.0-master 76 | - use correct versioning based on fedora package versioning guide 77 | 78 | * Sat Apr 15 2017 Michiel van Baak - pre-1.0 79 | - Initial RPM spec file 80 | -------------------------------------------------------------------------------- /test/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | de.widdix 8 | awsec2ssh-tests 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 13 | com.amazonaws 14 | aws-java-sdk-cloudformation 15 | test 16 | 17 | 18 | com.amazonaws 19 | aws-java-sdk-ec2 20 | test 21 | 22 | 23 | com.amazonaws 24 | aws-java-sdk-iam 25 | test 26 | 27 | 28 | com.amazonaws 29 | aws-java-sdk-sts 30 | test 31 | 32 | 33 | de.taimos 34 | httputils 35 | 1.10 36 | test 37 | 38 | 39 | com.evanlennick 40 | retry4j 41 | 0.6.2 42 | test 43 | 44 | 45 | com.jcraft 46 | jsch 47 | 0.1.54 48 | test 49 | 50 | 51 | junit 52 | junit 53 | 4.12 54 | test 55 | 56 | 57 | 58 | 59 | 60 | 61 | com.amazonaws 62 | aws-java-sdk-bom 63 | 1.11.883 64 | pom 65 | import 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | org.apache.maven.plugins 74 | maven-compiler-plugin 75 | 3.5.1 76 | 77 | 1.8 78 | 1.8 79 | 80 | 81 | 82 | org.apache.maven.plugins 83 | maven-surefire-plugin 84 | 2.20 85 | 86 | methods 87 | 2 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /test/src/test/java/de/widdix/awsec2ssh/ATest.java: -------------------------------------------------------------------------------- 1 | package de.widdix.awsec2ssh; 2 | 3 | import com.amazonaws.services.ec2.model.KeyPair; 4 | import com.evanlennick.retry4j.CallExecutor; 5 | import com.evanlennick.retry4j.CallResults; 6 | import com.evanlennick.retry4j.RetryConfig; 7 | import com.evanlennick.retry4j.RetryConfigBuilder; 8 | import com.jcraft.jsch.JSch; 9 | import com.jcraft.jsch.JSchException; 10 | import com.jcraft.jsch.Session; 11 | import org.junit.Assert; 12 | 13 | import java.time.temporal.ChronoUnit; 14 | import java.util.concurrent.Callable; 15 | import java.util.concurrent.atomic.AtomicInteger; 16 | 17 | public abstract class ATest { 18 | 19 | protected final T retry(final Callable callable) { 20 | final AtomicInteger t = new AtomicInteger(0); 21 | final Callable wrapper = () -> { 22 | try { 23 | return callable.call(); 24 | } catch (final Exception e) { 25 | System.out.println("retry[" + t.incrementAndGet() + "] exception: " + e.getMessage()); 26 | e.printStackTrace(System.out); 27 | System.out.println(); 28 | throw e; 29 | } 30 | }; 31 | final RetryConfig config = new RetryConfigBuilder() 32 | .retryOnAnyException() 33 | .withMaxNumberOfTries(30) 34 | .withDelayBetweenTries(10, ChronoUnit.SECONDS) 35 | .withFixedBackoff() 36 | .build(); 37 | final CallResults results = new CallExecutor(config).execute(wrapper); 38 | return (T) results.getResult(); 39 | } 40 | 41 | public static final class User { 42 | public final String userName; 43 | public final byte[] sshPrivateKeyBlob; 44 | public final String sshPublicKeyId; 45 | 46 | public User(final String userName, final byte[] sshPrivateKeyBlob, final String sshPublicKeyId) { 47 | super(); 48 | this.userName = userName; 49 | this.sshPrivateKeyBlob = sshPrivateKeyBlob; 50 | this.sshPublicKeyId = sshPublicKeyId; 51 | } 52 | } 53 | 54 | protected final void probeSSH(final String host, final User user) { 55 | final Callable callable = () -> { 56 | final JSch jsch = new JSch(); 57 | final Session session = jsch.getSession(user.userName, host); 58 | jsch.addIdentity(user.userName, user.sshPrivateKeyBlob, null, null); 59 | jsch.setConfig("StrictHostKeyChecking", "no"); // for testing this should be fine. adding the host key seems to be only possible via a file which is not very useful here 60 | session.connect(10000); 61 | session.disconnect(); 62 | return true; 63 | }; 64 | Assert.assertTrue("successful SSH connection", this.retry(callable)); 65 | } 66 | 67 | protected final void probeSSH(final String host, final KeyPair key) { 68 | final Callable callable = () -> { 69 | final JSch jsch = new JSch(); 70 | final Session session = jsch.getSession("ec2-user", host); 71 | jsch.addIdentity(key.getKeyName(), key.getKeyMaterial().getBytes(), null, null); 72 | jsch.setConfig("StrictHostKeyChecking", "no"); // for testing this should be fine. adding the host key seems to be only possible via a file which is not very useful here 73 | session.connect(10000); 74 | session.disconnect(); 75 | return true; 76 | }; 77 | Assert.assertTrue("successful SSH connection", this.retry(callable)); 78 | } 79 | 80 | protected final Session tunnelSSH(final String host, final KeyPair key, final Integer localPort, final String remoteHost, final Integer remotePort) throws JSchException { 81 | final JSch jsch = new JSch(); 82 | final Session session = jsch.getSession("ec2-user", host); 83 | jsch.addIdentity(key.getKeyName(), key.getKeyMaterial().getBytes(), null, null); 84 | jsch.setConfig("StrictHostKeyChecking", "no"); // for testing this should be fine. adding the host key seems to be only possible via a file which is not very useful here 85 | session.setPortForwardingL(localPort, remoteHost, remotePort); 86 | session.connect(10000); 87 | return session; 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /test/src/test/java/de/widdix/awsec2ssh/AAWSTest.java: -------------------------------------------------------------------------------- 1 | package de.widdix.awsec2ssh; 2 | 3 | import com.amazonaws.auth.*; 4 | import com.amazonaws.services.ec2.AmazonEC2; 5 | import com.amazonaws.services.ec2.AmazonEC2ClientBuilder; 6 | import com.amazonaws.services.ec2.model.*; 7 | import com.amazonaws.services.identitymanagement.AmazonIdentityManagement; 8 | import com.amazonaws.services.identitymanagement.AmazonIdentityManagementClientBuilder; 9 | import com.amazonaws.services.identitymanagement.model.*; 10 | import com.amazonaws.services.securitytoken.AWSSecurityTokenService; 11 | import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClientBuilder; 12 | import com.jcraft.jsch.JSch; 13 | import com.jcraft.jsch.JSchException; 14 | import com.jcraft.jsch.KeyPair; 15 | 16 | import java.io.ByteArrayOutputStream; 17 | import java.util.List; 18 | import java.util.UUID; 19 | 20 | public abstract class AAWSTest extends ATest { 21 | 22 | public final static String IAM_SESSION_NAME = "aws-ec2-ssh"; 23 | 24 | protected final AWSCredentialsProvider credentialsProvider; 25 | 26 | private AmazonEC2 ec2; 27 | 28 | private AmazonIdentityManagement iam; 29 | 30 | public AAWSTest() { 31 | super(); 32 | if (Config.has(Config.Key.IAM_ROLE_ARN)) { 33 | final AWSSecurityTokenService sts = AWSSecurityTokenServiceClientBuilder.standard().withCredentials(new DefaultAWSCredentialsProviderChain()).build(); 34 | this.credentialsProvider = new STSAssumeRoleSessionCredentialsProvider.Builder(Config.get(Config.Key.IAM_ROLE_ARN), IAM_SESSION_NAME).withStsClient(sts).build(); 35 | } else { 36 | this.credentialsProvider = new DefaultAWSCredentialsProviderChain(); 37 | } 38 | this.ec2 = AmazonEC2ClientBuilder.standard().withCredentials(this.credentialsProvider).build(); 39 | this.iam = AmazonIdentityManagementClientBuilder.standard().withCredentials(this.credentialsProvider).build(); 40 | } 41 | 42 | protected final User createUser(final String userName) throws JSchException { 43 | final JSch jsch = new JSch(); 44 | final KeyPair keyPair = KeyPair.genKeyPair(jsch, KeyPair.RSA, 2048); 45 | final ByteArrayOutputStream osPublicKey = new ByteArrayOutputStream(); 46 | final ByteArrayOutputStream osPrivateKey = new ByteArrayOutputStream(); 47 | keyPair.writePublicKey(osPublicKey, userName); 48 | keyPair.writePrivateKey(osPrivateKey); 49 | final byte[] sshPrivateKeyBlob = osPrivateKey.toByteArray(); 50 | final String sshPublicKeyBody = osPublicKey.toString(); 51 | this.iam.createUser(new CreateUserRequest().withUserName(userName)); 52 | final UploadSSHPublicKeyResult res = this.iam.uploadSSHPublicKey(new UploadSSHPublicKeyRequest().withUserName(userName).withSSHPublicKeyBody(sshPublicKeyBody)); 53 | return new User(userName, sshPrivateKeyBlob, res.getSSHPublicKey().getSSHPublicKeyId()); 54 | } 55 | 56 | protected final void deleteUser(final String userName) { 57 | if (Config.get(Config.Key.DELETION_POLICY).equals("delete")) { 58 | final ListSSHPublicKeysResult res = this.iam.listSSHPublicKeys(new ListSSHPublicKeysRequest().withUserName(userName)); 59 | this.iam.deleteSSHPublicKey(new DeleteSSHPublicKeyRequest().withUserName(userName).withSSHPublicKeyId(res.getSSHPublicKeys().get(0).getSSHPublicKeyId())); 60 | this.iam.deleteUser(new DeleteUserRequest().withUserName(userName)); 61 | } 62 | } 63 | 64 | protected final Vpc getDefaultVPC() { 65 | final DescribeVpcsResult res = this.ec2.describeVpcs(new DescribeVpcsRequest().withFilters(new Filter().withName("isDefault").withValues("true"))); 66 | return res.getVpcs().get(0); 67 | } 68 | 69 | protected final List getDefaultSubnets() { 70 | final DescribeSubnetsResult res = this.ec2.describeSubnets(new DescribeSubnetsRequest().withFilters(new Filter().withName("defaultForAz").withValues("true"))); 71 | return res.getSubnets(); 72 | } 73 | 74 | protected final SecurityGroup getDefaultSecurityGroup() { 75 | final Vpc vpc = this.getDefaultVPC(); 76 | final DescribeSecurityGroupsResult res = this.ec2.describeSecurityGroups(new DescribeSecurityGroupsRequest().withFilters( 77 | new Filter().withName("vpc-id").withValues(vpc.getVpcId()), 78 | new Filter().withName("group-name").withValues("default") 79 | )); 80 | return res.getSecurityGroups().get(0); 81 | } 82 | 83 | protected final String random8String() { 84 | final String uuid = UUID.randomUUID().toString().replace("-", "").toLowerCase(); 85 | final int beginIndex = (int) (Math.random() * (uuid.length() - 7)); 86 | final int endIndex = beginIndex + 7; 87 | return "r" + uuid.substring(beginIndex, endIndex); // must begin [a-z] 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | show_help() { 4 | cat << EOF 5 | Usage: ${0##*/} [-hv] [-a ARN] [-i GROUP,GROUP,...] [-l GROUP,GROUP,...] [-s GROUP] [-p PROGRAM] [-u "ARGUMENTS"] [-r RELEASE] 6 | Install import_users.sh and authorized_key_commands. 7 | 8 | -h display this help and exit 9 | -v verbose mode. 10 | 11 | -a arn Assume a role before contacting AWS IAM to get users and keys. 12 | This can be used if you define your users in one AWS account, while the EC2 13 | instance you use this script runs in another. 14 | -i group,group Which IAM groups have access to this instance 15 | Comma seperated list of IAM groups. Leave empty for all available IAM users 16 | -l group,group Give the users these local UNIX groups 17 | Comma seperated list 18 | -s group,group Specify IAM group(s) for users who should be given sudo privileges, or leave 19 | empty to not change sudo access, or give it the value '##ALL##' to have all 20 | users be given sudo rights. 21 | Comma seperated list 22 | -p program Specify your useradd program to use. 23 | Defaults to '/usr/sbin/useradd' 24 | -u "useradd args" Specify arguments to use with useradd. 25 | Defaults to '--create-home --shell /bin/bash' 26 | -r release Specify a release of aws-ec2-ssh to download from GitHub. This argument is 27 | passed to \`git clone -b\` and so works with branches and tags. 28 | Defaults to 'master' 29 | 30 | 31 | EOF 32 | } 33 | 34 | export SSHD_CONFIG_FILE="/etc/ssh/sshd_config" 35 | export AUTHORIZED_KEYS_COMMAND_FILE="/opt/authorized_keys_command.sh" 36 | export IMPORT_USERS_SCRIPT_FILE="/opt/import_users.sh" 37 | export MAIN_CONFIG_FILE="/etc/aws-ec2-ssh.conf" 38 | 39 | IAM_GROUPS="" 40 | SUDO_GROUPS="" 41 | LOCAL_GROUPS="" 42 | ASSUME_ROLE="" 43 | USERADD_PROGRAM="" 44 | USERADD_ARGS="" 45 | USERDEL_PROGRAM="" 46 | USERDEL_ARGS="" 47 | RELEASE="master" 48 | 49 | while getopts :hva:i:l:s:p:u:d:f:r: opt 50 | do 51 | case $opt in 52 | h) 53 | show_help 54 | exit 0 55 | ;; 56 | i) 57 | IAM_GROUPS="$OPTARG" 58 | ;; 59 | s) 60 | SUDO_GROUPS="$OPTARG" 61 | ;; 62 | l) 63 | LOCAL_GROUPS="$OPTARG" 64 | ;; 65 | v) 66 | set -x 67 | ;; 68 | a) 69 | ASSUME_ROLE="$OPTARG" 70 | ;; 71 | p) 72 | USERADD_PROGRAM="$OPTARG" 73 | ;; 74 | u) 75 | USERADD_ARGS="$OPTARG" 76 | ;; 77 | d) 78 | USERDEL_PROGRAM="$OPTARG" 79 | ;; 80 | f) 81 | USERDEL_ARGS="$OPTARG" 82 | ;; 83 | r) 84 | RELEASE="$OPTARG" 85 | ;; 86 | \?) 87 | echo "Invalid option: -$OPTARG" >&2 88 | show_help 89 | exit 1 90 | ;; 91 | :) 92 | echo "Option -$OPTARG requires an argument." >&2 93 | show_help 94 | exit 1 95 | esac 96 | done 97 | 98 | export IAM_GROUPS 99 | export SUDO_GROUPS 100 | export LOCAL_GROUPS 101 | export ASSUME_ROLE 102 | export USERADD_PROGRAM 103 | export USERADD_ARGS 104 | export USERDEL_PROGRAM 105 | export USERDEL_ARGS 106 | 107 | # check if AWS CLI exists 108 | if ! [ -x "$(which aws)" ]; then 109 | echo "aws executable not found - exiting!" 110 | exit 1 111 | fi 112 | 113 | # check if git exists 114 | if ! [ -x "$(which git)" ]; then 115 | echo "git executable not found - exiting!" 116 | exit 1 117 | fi 118 | 119 | tmpdir=$(mktemp -d) 120 | 121 | cd "$tmpdir" 122 | 123 | git clone -b "$RELEASE" https://github.com/widdix/aws-ec2-ssh.git 124 | 125 | cd "$tmpdir/aws-ec2-ssh" 126 | 127 | cp authorized_keys_command.sh $AUTHORIZED_KEYS_COMMAND_FILE 128 | cp import_users.sh $IMPORT_USERS_SCRIPT_FILE 129 | 130 | if [ "${IAM_GROUPS}" != "" ] 131 | then 132 | echo "IAM_AUTHORIZED_GROUPS=\"${IAM_GROUPS}\"" >> $MAIN_CONFIG_FILE 133 | fi 134 | 135 | if [ "${SUDO_GROUPS}" != "" ] 136 | then 137 | echo "SUDOERS_GROUPS=\"${SUDO_GROUPS}\"" >> $MAIN_CONFIG_FILE 138 | fi 139 | 140 | if [ "${LOCAL_GROUPS}" != "" ] 141 | then 142 | echo "LOCAL_GROUPS=\"${LOCAL_GROUPS}\"" >> $MAIN_CONFIG_FILE 143 | fi 144 | 145 | if [ "${ASSUME_ROLE}" != "" ] 146 | then 147 | echo "ASSUMEROLE=\"${ASSUME_ROLE}\"" >> $MAIN_CONFIG_FILE 148 | fi 149 | 150 | if [ "${USERADD_PROGRAM}" != "" ] 151 | then 152 | echo "USERADD_PROGRAM=\"${USERADD_PROGRAM}\"" >> $MAIN_CONFIG_FILE 153 | fi 154 | 155 | if [ "${USERADD_ARGS}" != "" ] 156 | then 157 | echo "USERADD_ARGS=\"${USERADD_ARGS}\"" >> $MAIN_CONFIG_FILE 158 | fi 159 | 160 | if [ "${USERDEL_PROGRAM}" != "" ] 161 | then 162 | echo "USERDEL_PROGRAM=\"${USERDEL_PROGRAM}\"" >> $MAIN_CONFIG_FILE 163 | fi 164 | 165 | if [ "${USERDEL_ARGS}" != "" ] 166 | then 167 | echo "USERDEL_ARGS=\"${USERDEL_ARGS}\"" >> $MAIN_CONFIG_FILE 168 | fi 169 | 170 | ./install_configure_selinux.sh 171 | 172 | ./install_configure_sshd.sh 173 | 174 | cat > /etc/cron.d/import_users << EOF 175 | SHELL=/bin/bash 176 | PATH=/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/sbin:/opt/aws/bin 177 | MAILTO=root 178 | HOME=/ 179 | */10 * * * * root $IMPORT_USERS_SCRIPT_FILE 180 | EOF 181 | chmod 0644 /etc/cron.d/import_users 182 | 183 | $IMPORT_USERS_SCRIPT_FILE 184 | 185 | ./install_restart_sshd.sh 186 | -------------------------------------------------------------------------------- /test/src/test/java/de/widdix/awsec2ssh/TestShowcase.java: -------------------------------------------------------------------------------- 1 | package de.widdix.awsec2ssh; 2 | 3 | import com.amazonaws.services.cloudformation.model.Parameter; 4 | import org.junit.Test; 5 | 6 | public class TestShowcase extends ACloudFormationTest { 7 | 8 | @Test 9 | public void testCentOS() throws Exception { 10 | final String stackName = "showcase-" + this.random8String(); 11 | final String userName = "user-" + this.random8String(); 12 | try { 13 | final User user = this.createUser(userName); 14 | try { 15 | this.createStack(stackName, 16 | "showcase.yaml", 17 | new Parameter().withParameterKey("VPC").withParameterValue(this.getDefaultVPC().getVpcId()), 18 | new Parameter().withParameterKey("Subnet").withParameterValue(this.getDefaultSubnets().get(0).getSubnetId()), 19 | new Parameter().withParameterKey("OS").withParameterValue("CentOS") 20 | ); 21 | final String host = this.getStackOutputValue(stackName, "PublicName"); 22 | this.probeSSH(host, user); 23 | } finally { 24 | this.deleteStack(stackName); 25 | } 26 | } finally { 27 | this.deleteUser(userName); 28 | } 29 | } 30 | 31 | @Test 32 | public void testRHEL() throws Exception { 33 | final String stackName = "showcase-" + this.random8String(); 34 | final String userName = "user-" + this.random8String(); 35 | try { 36 | final User user = this.createUser(userName); 37 | try { 38 | this.createStack(stackName, 39 | "showcase.yaml", 40 | new Parameter().withParameterKey("VPC").withParameterValue(this.getDefaultVPC().getVpcId()), 41 | new Parameter().withParameterKey("Subnet").withParameterValue(this.getDefaultSubnets().get(0).getSubnetId()), 42 | new Parameter().withParameterKey("OS").withParameterValue("RHEL") 43 | ); 44 | final String host = this.getStackOutputValue(stackName, "PublicName"); 45 | this.probeSSH(host, user); 46 | } finally { 47 | this.deleteStack(stackName); 48 | } 49 | } finally { 50 | this.deleteUser(userName); 51 | } 52 | } 53 | 54 | @Test 55 | public void testSUSELinuxEnterpriseServer() throws Exception { 56 | final String stackName = "showcase-" + this.random8String(); 57 | final String userName = "user-" + this.random8String(); 58 | try { 59 | final User user = this.createUser(userName); 60 | try { 61 | this.createStack(stackName, 62 | "showcase.yaml", 63 | new Parameter().withParameterKey("VPC").withParameterValue(this.getDefaultVPC().getVpcId()), 64 | new Parameter().withParameterKey("Subnet").withParameterValue(this.getDefaultSubnets().get(0).getSubnetId()), 65 | new Parameter().withParameterKey("OS").withParameterValue("SUSELinuxEnterpriseServer") 66 | ); 67 | final String host = this.getStackOutputValue(stackName, "PublicName"); 68 | this.probeSSH(host, user); 69 | } finally { 70 | this.deleteStack(stackName); 71 | } 72 | } finally { 73 | this.deleteUser(userName); 74 | } 75 | } 76 | 77 | @Test 78 | public void testUbuntu() throws Exception { 79 | final String stackName = "showcase-" + this.random8String(); 80 | final String userName = "user-" + this.random8String(); 81 | try { 82 | final User user = this.createUser(userName); 83 | try { 84 | this.createStack(stackName, 85 | "showcase.yaml", 86 | new Parameter().withParameterKey("VPC").withParameterValue(this.getDefaultVPC().getVpcId()), 87 | new Parameter().withParameterKey("Subnet").withParameterValue(this.getDefaultSubnets().get(0).getSubnetId()), 88 | new Parameter().withParameterKey("OS").withParameterValue("Ubuntu") 89 | ); 90 | final String host = this.getStackOutputValue(stackName, "PublicName"); 91 | this.probeSSH(host, user); 92 | } finally { 93 | this.deleteStack(stackName); 94 | } 95 | } finally { 96 | this.deleteUser(userName); 97 | } 98 | } 99 | 100 | @Test 101 | public void testAmazonLinux2() throws Exception { 102 | final String stackName = "showcase-" + this.random8String(); 103 | final String userName = "user-" + this.random8String(); 104 | try { 105 | final User user = this.createUser(userName); 106 | try { 107 | this.createStack(stackName, 108 | "showcase.yaml", 109 | new Parameter().withParameterKey("VPC").withParameterValue(this.getDefaultVPC().getVpcId()), 110 | new Parameter().withParameterKey("Subnet").withParameterValue(this.getDefaultSubnets().get(0).getSubnetId()), 111 | new Parameter().withParameterKey("OS").withParameterValue("AmazonLinux2") 112 | ); 113 | final String host = this.getStackOutputValue(stackName, "PublicName"); 114 | this.probeSSH(host, user); 115 | } finally { 116 | this.deleteStack(stackName); 117 | } 118 | } finally { 119 | this.deleteUser(userName); 120 | } 121 | } 122 | 123 | @Test 124 | public void testDefaultAmazonLinux() throws Exception { 125 | final String stackName = "showcase-" + this.random8String(); 126 | final String userName = "user-" + this.random8String(); 127 | try { 128 | final User user = this.createUser(userName); 129 | try { 130 | this.createStack(stackName, 131 | "showcase.yaml", 132 | new Parameter().withParameterKey("VPC").withParameterValue(this.getDefaultVPC().getVpcId()), 133 | new Parameter().withParameterKey("Subnet").withParameterValue(this.getDefaultSubnets().get(0).getSubnetId()) 134 | ); 135 | final String host = this.getStackOutputValue(stackName, "PublicName"); 136 | this.probeSSH(host, user); 137 | } finally { 138 | this.deleteStack(stackName); 139 | } 140 | } finally { 141 | this.deleteUser(userName); 142 | } 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /test/src/test/java/de/widdix/awsec2ssh/ACloudFormationTest.java: -------------------------------------------------------------------------------- 1 | package de.widdix.awsec2ssh; 2 | 3 | import com.amazonaws.AmazonServiceException; 4 | import com.amazonaws.services.cloudformation.AmazonCloudFormation; 5 | import com.amazonaws.services.cloudformation.AmazonCloudFormationClientBuilder; 6 | import com.amazonaws.services.cloudformation.model.*; 7 | 8 | import java.io.IOException; 9 | import java.nio.charset.Charset; 10 | import java.nio.file.Files; 11 | import java.nio.file.Paths; 12 | import java.util.*; 13 | 14 | public abstract class ACloudFormationTest extends AAWSTest { 15 | 16 | public static String readFile(String path, Charset encoding) { 17 | try { 18 | byte[] encoded = Files.readAllBytes(Paths.get(path)); 19 | return new String(encoded, encoding); 20 | } catch (final IOException e) { 21 | throw new RuntimeException(e); 22 | } 23 | } 24 | 25 | private final AmazonCloudFormation cf = AmazonCloudFormationClientBuilder.standard().withCredentials(this.credentialsProvider).build(); 26 | 27 | public ACloudFormationTest() { 28 | super(); 29 | } 30 | 31 | protected final void createStack(final String stackName, final String template, final Parameter...parameters) { 32 | final String dir = Config.get(Config.Key.TEMPLATE_DIR); 33 | final String body = readFile(dir + template, Charset.forName("UTF-8")); 34 | final CreateStackRequest req = new CreateStackRequest() 35 | .withStackName(stackName) 36 | .withParameters(parameters) 37 | .withCapabilities(Capability.CAPABILITY_IAM) 38 | .withTemplateBody(body); 39 | this.cf.createStack(req); 40 | this.waitForStack(stackName, FinalStatus.CREATE_COMPLETE); 41 | } 42 | 43 | protected enum FinalStatus { 44 | CREATE_COMPLETE(StackStatus.CREATE_COMPLETE, false, true, StackStatus.CREATE_IN_PROGRESS), 45 | DELETE_COMPLETE(StackStatus.DELETE_COMPLETE, true, false, StackStatus.DELETE_IN_PROGRESS); 46 | 47 | private final StackStatus finalStatus; 48 | private final boolean notFoundIsFinalStatus; 49 | private final boolean notFoundIsIntermediateStatus; 50 | private final Set intermediateStatus; 51 | 52 | FinalStatus(StackStatus finalStatus, boolean notFoundIsFinalStatus, boolean notFoundIsIntermediateStatus, StackStatus...intermediateStatus) { 53 | this.finalStatus = finalStatus; 54 | this.notFoundIsFinalStatus = notFoundIsFinalStatus; 55 | this.notFoundIsIntermediateStatus = notFoundIsIntermediateStatus; 56 | this.intermediateStatus = new HashSet<>(Arrays.asList(intermediateStatus)); 57 | } 58 | } 59 | 60 | private List getStackEvents(final String stackName) { 61 | final List events = new ArrayList<>(); 62 | String nextToken = null; 63 | do { 64 | try { 65 | final DescribeStackEventsResult res = this.cf.describeStackEvents(new DescribeStackEventsRequest().withStackName(stackName).withNextToken(nextToken)); 66 | events.addAll(res.getStackEvents()); 67 | nextToken = res.getNextToken(); 68 | } catch (final AmazonServiceException e) { 69 | if (e.getErrorMessage().equals("Stack [" + stackName + "] does not exist")) { 70 | nextToken = null; 71 | } else { 72 | throw e; 73 | } 74 | } 75 | } while (nextToken != null); 76 | Collections.reverse(events); 77 | return events; 78 | } 79 | 80 | private void waitForStack(final String stackName, final FinalStatus finalStackStatus) { 81 | System.out.println("waitForStack[" + stackName + "]: to reach status " + finalStackStatus.finalStatus); 82 | final List eventsDisplayed = new ArrayList<>(); 83 | while (true) { 84 | try { 85 | Thread.sleep(5000); 86 | } catch (final InterruptedException e) { 87 | // continue 88 | } 89 | final List events = getStackEvents(stackName); 90 | for (final StackEvent event : events) { 91 | boolean displayed = false; 92 | for (final StackEvent eventDisplayed : eventsDisplayed) { 93 | if (event.getEventId().equals(eventDisplayed.getEventId())) { 94 | displayed = true; 95 | } 96 | } 97 | if (!displayed) { 98 | System.out.println("waitForStack[" + stackName + "]: " + event.getTimestamp().toString() + " " + event.getLogicalResourceId() + " " + event.getResourceStatus() + " " + event.getResourceStatusReason()); 99 | eventsDisplayed.add(event); 100 | } 101 | } 102 | try { 103 | final DescribeStacksResult res = this.cf.describeStacks(new DescribeStacksRequest().withStackName(stackName)); 104 | final StackStatus currentStatus = StackStatus.fromValue(res.getStacks().get(0).getStackStatus()); 105 | if (finalStackStatus.finalStatus == currentStatus) { 106 | System.out.println("waitForStack[" + stackName + "]: final status reached."); 107 | return; 108 | } else { 109 | if (finalStackStatus.intermediateStatus.contains(currentStatus)) { 110 | System.out.println("waitForStack[" + stackName + "]: continue to wait (still in intermediate status " + currentStatus + ") ..."); 111 | } else { 112 | throw new RuntimeException("waitForStack[" + stackName + "]: reached invalid intermediate status " + currentStatus + "."); 113 | } 114 | } 115 | } catch (final AmazonServiceException e) { 116 | if (e.getErrorMessage().equals("Stack with id " + stackName + " does not exist")) { 117 | if (finalStackStatus.notFoundIsFinalStatus) { 118 | System.out.println("waitForStack[" + stackName + "]: final reached (not found)."); 119 | return; 120 | } else { 121 | if (finalStackStatus.notFoundIsIntermediateStatus) { 122 | System.out.println("waitForStack[" + stackName + "]: continue to wait (stack not found) ..."); 123 | } else { 124 | throw new RuntimeException("waitForStack[" + stackName + "]: stack not found."); 125 | } 126 | } 127 | } else { 128 | throw e; 129 | } 130 | } 131 | } 132 | } 133 | 134 | protected final Map getStackOutputs(final String stackName) { 135 | final DescribeStacksResult res = this.cf.describeStacks(new DescribeStacksRequest().withStackName(stackName)); 136 | final List outputs = res.getStacks().get(0).getOutputs(); 137 | final Map map = new HashMap<>(outputs.size()); 138 | for (final Output output : outputs) { 139 | map.put(output.getOutputKey(), output.getOutputValue()); 140 | } 141 | return map; 142 | } 143 | 144 | protected final String getStackOutputValue(final String stackName, final String outputKey) { 145 | return this.getStackOutputs(stackName).get(outputKey); 146 | } 147 | 148 | protected final void deleteStack(final String stackName) { 149 | if (Config.get(Config.Key.DELETION_POLICY).equals("delete")) { 150 | this.cf.deleteStack(new DeleteStackRequest().withStackName(stackName)); 151 | this.waitForStack(stackName, FinalStatus.DELETE_COMPLETE); 152 | } 153 | } 154 | 155 | } 156 | -------------------------------------------------------------------------------- /test/src/test/java/de/widdix/awsec2ssh/TestShowcaseRPM.java: -------------------------------------------------------------------------------- 1 | package de.widdix.awsec2ssh; 2 | 3 | import com.amazonaws.services.cloudformation.model.Parameter; 4 | import org.junit.Test; 5 | 6 | public class TestShowcaseRPM extends ACloudFormationTest { 7 | 8 | // TODO make Version parameter configurable via ENV variable 9 | 10 | @Test 11 | public void testCentOS() throws Exception { 12 | final String stackName = "showcase-rpm-" + this.random8String(); 13 | final String userName = "user-" + this.random8String(); 14 | try { 15 | final User user = this.createUser(userName); 16 | try { 17 | if (Config.has(Config.Key.VERSION)) { 18 | this.createStack(stackName, 19 | "showcase-rpm.yaml", 20 | new Parameter().withParameterKey("VPC").withParameterValue(this.getDefaultVPC().getVpcId()), 21 | new Parameter().withParameterKey("Subnet").withParameterValue(this.getDefaultSubnets().get(0).getSubnetId()), 22 | new Parameter().withParameterKey("OS").withParameterValue("CentOS"), 23 | new Parameter().withParameterKey("Version").withParameterValue(Config.get(Config.Key.VERSION)) 24 | ); 25 | } else { 26 | this.createStack(stackName, 27 | "showcase-rpm.yaml", 28 | new Parameter().withParameterKey("VPC").withParameterValue(this.getDefaultVPC().getVpcId()), 29 | new Parameter().withParameterKey("Subnet").withParameterValue(this.getDefaultSubnets().get(0).getSubnetId()), 30 | new Parameter().withParameterKey("OS").withParameterValue("CentOS") 31 | ); 32 | } 33 | final String host = this.getStackOutputValue(stackName, "PublicName"); 34 | this.probeSSH(host, user); 35 | } finally { 36 | this.deleteStack(stackName); 37 | } 38 | } finally { 39 | this.deleteUser(userName); 40 | } 41 | } 42 | 43 | @Test 44 | public void testRHEL() throws Exception { 45 | final String stackName = "showcase-rpm-" + this.random8String(); 46 | final String userName = "user-" + this.random8String(); 47 | try { 48 | final User user = this.createUser(userName); 49 | try { 50 | if (Config.has(Config.Key.VERSION)) { 51 | this.createStack(stackName, 52 | "showcase-rpm.yaml", 53 | new Parameter().withParameterKey("VPC").withParameterValue(this.getDefaultVPC().getVpcId()), 54 | new Parameter().withParameterKey("Subnet").withParameterValue(this.getDefaultSubnets().get(0).getSubnetId()), 55 | new Parameter().withParameterKey("OS").withParameterValue("RHEL"), 56 | new Parameter().withParameterKey("Version").withParameterValue(Config.get(Config.Key.VERSION)) 57 | ); 58 | } else { 59 | this.createStack(stackName, 60 | "showcase-rpm.yaml", 61 | new Parameter().withParameterKey("VPC").withParameterValue(this.getDefaultVPC().getVpcId()), 62 | new Parameter().withParameterKey("Subnet").withParameterValue(this.getDefaultSubnets().get(0).getSubnetId()), 63 | new Parameter().withParameterKey("OS").withParameterValue("RHEL") 64 | ); 65 | } 66 | final String host = this.getStackOutputValue(stackName, "PublicName"); 67 | this.probeSSH(host, user); 68 | } finally { 69 | this.deleteStack(stackName); 70 | } 71 | } finally { 72 | this.deleteUser(userName); 73 | } 74 | } 75 | 76 | @Test 77 | public void testSUSELinuxEnterpriseServer() throws Exception { 78 | final String stackName = "showcase-rpm-" + this.random8String(); 79 | final String userName = "user-" + this.random8String(); 80 | try { 81 | final User user = this.createUser(userName); 82 | try { 83 | if (Config.has(Config.Key.VERSION)) { 84 | this.createStack(stackName, 85 | "showcase-rpm.yaml", 86 | new Parameter().withParameterKey("VPC").withParameterValue(this.getDefaultVPC().getVpcId()), 87 | new Parameter().withParameterKey("Subnet").withParameterValue(this.getDefaultSubnets().get(0).getSubnetId()), 88 | new Parameter().withParameterKey("OS").withParameterValue("SUSELinuxEnterpriseServer"), 89 | new Parameter().withParameterKey("Version").withParameterValue(Config.get(Config.Key.VERSION)) 90 | ); 91 | } else { 92 | this.createStack(stackName, 93 | "showcase-rpm.yaml", 94 | new Parameter().withParameterKey("VPC").withParameterValue(this.getDefaultVPC().getVpcId()), 95 | new Parameter().withParameterKey("Subnet").withParameterValue(this.getDefaultSubnets().get(0).getSubnetId()), 96 | new Parameter().withParameterKey("OS").withParameterValue("SUSELinuxEnterpriseServer") 97 | ); 98 | } 99 | final String host = this.getStackOutputValue(stackName, "PublicName"); 100 | this.probeSSH(host, user); 101 | } finally { 102 | this.deleteStack(stackName); 103 | } 104 | } finally { 105 | this.deleteUser(userName); 106 | } 107 | } 108 | 109 | @Test 110 | public void testAmazonLinux2() throws Exception { 111 | final String stackName = "showcase-rpm-" + this.random8String(); 112 | final String userName = "user-" + this.random8String(); 113 | try { 114 | final User user = this.createUser(userName); 115 | try { 116 | if (Config.has(Config.Key.VERSION)) { 117 | this.createStack(stackName, 118 | "showcase-rpm.yaml", 119 | new Parameter().withParameterKey("VPC").withParameterValue(this.getDefaultVPC().getVpcId()), 120 | new Parameter().withParameterKey("Subnet").withParameterValue(this.getDefaultSubnets().get(0).getSubnetId()), 121 | new Parameter().withParameterKey("OS").withParameterValue("AmazonLinux2"), 122 | new Parameter().withParameterKey("Version").withParameterValue(Config.get(Config.Key.VERSION)) 123 | ); 124 | } else { 125 | this.createStack(stackName, 126 | "showcase-rpm.yaml", 127 | new Parameter().withParameterKey("VPC").withParameterValue(this.getDefaultVPC().getVpcId()), 128 | new Parameter().withParameterKey("Subnet").withParameterValue(this.getDefaultSubnets().get(0).getSubnetId()), 129 | new Parameter().withParameterKey("OS").withParameterValue("AmazonLinux2") 130 | ); 131 | } 132 | final String host = this.getStackOutputValue(stackName, "PublicName"); 133 | this.probeSSH(host, user); 134 | } finally { 135 | this.deleteStack(stackName); 136 | } 137 | } finally { 138 | this.deleteUser(userName); 139 | } 140 | } 141 | 142 | @Test 143 | public void testDefaultAmazonLinux() throws Exception { 144 | final String stackName = "showcase-rpm-" + this.random8String(); 145 | final String userName = "user-" + this.random8String(); 146 | try { 147 | final User user = this.createUser(userName); 148 | try { 149 | if (Config.has(Config.Key.VERSION)) { 150 | this.createStack(stackName, 151 | "showcase-rpm.yaml", 152 | new Parameter().withParameterKey("VPC").withParameterValue(this.getDefaultVPC().getVpcId()), 153 | new Parameter().withParameterKey("Subnet").withParameterValue(this.getDefaultSubnets().get(0).getSubnetId()), 154 | new Parameter().withParameterKey("Version").withParameterValue(Config.get(Config.Key.VERSION)) 155 | ); 156 | } else { 157 | this.createStack(stackName, 158 | "showcase-rpm.yaml", 159 | new Parameter().withParameterKey("VPC").withParameterValue(this.getDefaultVPC().getVpcId()), 160 | new Parameter().withParameterKey("Subnet").withParameterValue(this.getDefaultSubnets().get(0).getSubnetId()) 161 | ); 162 | } 163 | final String host = this.getStackOutputValue(stackName, "PublicName"); 164 | this.probeSSH(host, user); 165 | } finally { 166 | this.deleteStack(stackName); 167 | } 168 | } finally { 169 | this.deleteUser(userName); 170 | } 171 | } 172 | 173 | } 174 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Manage AWS EC2 SSH access with IAM 2 | 3 | > This project is no longer maintained. AWS offers two alternatives to solve similar needs: 4 | > * [Amazon EC2 Instance Connect](https://aws.amazon.com/blogs/compute/new-using-amazon-ec2-instance-connect-for-ssh-access-to-your-ec2-instances/) 5 | > * [AWS Systems Manager Session Manager ](https://aws.amazon.com/de/blogs/aws/new-session-manager/) 6 | 7 | Use your IAM user's public SSH key to get access via SSH to an **EC2 instance** running 8 | * Amazon Linux 2018.03 9 | * Amazon Linux 2 10 | * Ubuntu 16.04 11 | * SUSE Linux Enterprise Server 12 SP3 12 | * RHEL 7.4 13 | * CentOS 7 14 | 15 | `aws-ec2-ssh` depends on the [AWS CLI](https://aws.amazon.com/cli/) and `git` if you use the `install.sh` script. 16 | 17 | ## How does it work 18 | 19 | A picture is worth a thousand words: 20 | 21 | ![Architecture](./docs/architecture.png?raw=true "Architecture") 22 | 23 | * On first start, all IAM users are imported and local UNIX users are created 24 | * The import also runs every 10 minutes (via cron - calls [`import_users.sh`](./import_users.sh)) 25 | * You can control which IAM users get a local UNIX user and are therefore able to login 26 | * all (default) 27 | * only those in specific IAM groups 28 | * You can control which IAM users are given sudo access 29 | * none (default) 30 | * all 31 | * only those in a specific IAM group 32 | * You can specify the local UNIX groups for the local UNIX users 33 | * You can assume a role before contacting AWS IAM to get users and keys (e.g. if your IAM users are in another AWS account) 34 | * On every SSH login, the EC2 instance tries to fetch the public key(s) from IAM using sshd's `AuthorizedKeysCommand` 35 | * As soon as the public SSH key is deleted from the IAM user a login is no longer possible 36 | 37 | ### Demo with CloudFormation 38 | 39 | 1. Upload your public SSH key to IAM: 40 | 1. Open the Users section in the [IAM Management Console](https://console.aws.amazon.com/iam/home#users) 41 | 2. Click the row with your user 42 | 3. Select the **Security Credentials** tab 43 | 4. Click the **Upload SSH public key** button at the bottom of the page 44 | 5. Paste your public SSH key into the text-area and click the **Upload SSH public key** button to save 45 | 2. Create a CloudFormation stack based on the `showcase.yaml` template 46 | 3. Wait until the stack status is `CREATE_COMPLETE` 47 | 4. Copy the `PublicName` from the stack's outputs 48 | 5. Connect to the EC2 instance via `ssh $Username@$PublicName` with `$Username` being your IAM user, and `$PublicName` with the stack's output 49 | 50 | ## How to integrate this system into your environment 51 | 52 | ### Install via RPM 53 | 54 | 1. Upload your public SSH key to IAM: 55 | 1. Open the Users section in the [IAM Management Console](https://console.aws.amazon.com/iam/home#users) 56 | 2. Click the row with your user 57 | 3. Select the **Security Credentials** tab 58 | 4. Click the **Upload SSH public key** button at the bottom of the page 59 | 5. Paste your public SSH key into the text-area and click the **Upload SSH public key** button to save 60 | 2. Attach the IAM permissions defined in [`iam_ssh_policy.json`](./iam_ssh_policy.json) to the EC2 instances (by creating an IAM role and an Instance Profile) 61 | 3. Install the RPM1: `rpm -i https://s3-eu-west-1.amazonaws.com/widdix-aws-ec2-ssh-releases-eu-west-1/aws-ec2-ssh-1.9.2-1.el7.centos.noarch.rpm` 62 | 4. The configuration file is placed into `/etc/aws-ec2-ssh.conf` 63 | 5. The RPM creates a crontab file to run import_users.sh every 10 minutes. This file is placed in `/etc/cron.d/import_users` 64 | 65 | > 1Check the [releases](https://github.com/widdix/aws-ec2-ssh/releases) and use the latest released RPM. 66 | 67 | ### Install via install.sh script 68 | 69 | 1. Upload your public SSH key to IAM: 70 | 1. Open the Users section in the [IAM Management Console](https://console.aws.amazon.com/iam/home#users) 71 | 2. Click the row with your user 72 | 3. Select the **Security Credentials** tab 73 | 4. Click the **Upload SSH public key** button at the bottom of the page 74 | 5. Paste your public SSH key into the text-area and click the **Upload SSH public key** button to save 75 | 2. Attach the IAM permissions defined in [`iam_ssh_policy.json`](./iam_ssh_policy.json) to the EC2 instances (by creating an IAM role and an Instance Profile) 76 | 3. Run the `install.sh` script as `root` on the EC2 instances. Run `install.sh -h` for help. 77 | 4. The configuration file is placed into `/etc/aws-ec2-ssh.conf` 78 | 5. Connect to your EC2 instances now using `ssh $Username@$PublicName` with `$Username` being your IAM user, and `$PublicName` being your server's name or IP address 79 | 80 | ## IAM user names and Linux user names 81 | 82 | Allowed characters for IAM user names are: 83 | > alphanumeric, including the following common characters: plus (+), equal (=), comma (,), period (.), at (@), underscore (_), and hyphen (-). 84 | 85 | Allowed characters for Linux user names are (POSIX ("Portable Operating System Interface for Unix") standard (IEEE Standard 1003.1 2008)): 86 | > alphanumeric, including the following common characters: period (.), underscore (_), and hyphen (-). 87 | 88 | Therefore, characters that are allowed in IAM user names but not in Linux user names: 89 | > plus (+), equal (=), comma (,), at (@). 90 | 91 | This solution will use the following mapping for those special characters when creating users: 92 | * `+` => `.plus.` 93 | * `=` => `.equal.` 94 | * `,` => `.comma.` 95 | * `@` => `.at.` 96 | 97 | So instead of `name@email.com` you will need to use `name.at.email.com` when login via SSH. 98 | 99 | Linux user names may only be up to 32 characters long. 100 | 101 | ## Configuration 102 | 103 | There are a couple of things you can configure by editing/creating the file `/etc/aws-ec2-ssh.conf` and adding 104 | one or more of the following lines: 105 | 106 | ``` 107 | ASSUMEROLE="IAM-role-arn" # IAM Role ARN for multi account. See below for more info 108 | IAM_AUTHORIZED_GROUPS="GROUPNAMES" # Comma separated list of IAM groups to import 109 | SUDOERS_GROUPS="GROUPNAMES" # Comma seperated list of IAM groups that should have sudo access or `##ALL##` to allow all users 110 | IAM_AUTHORIZED_GROUPS_TAG="KeyTag" # Key Tag of EC2 that contains a Comma separated list of IAM groups to import - IAM_AUTHORIZED_GROUPS_TAG will override IAM_AUTHORIZED_GROUPS, you can use only one of them 111 | SUDOERS_GROUPS_TAG="KeyTag" # Key Tag of EC2 that contains a Comma separated list of IAM groups that should have sudo access - SUDOERS_GROUPS_TAG will override SUDOERS_GROUPS, you can use only one of them 112 | SUDOERSGROUP="GROUPNAME" # Deprecated! IAM group that should have sudo access. Please use SUDOERS_GROUPS as this variable will be removed in future release. 113 | LOCAL_MARKER_GROUP="iam-synced-users" # Dedicated UNIX group to mark imported users. Used for deleting removed IAM users 114 | LOCAL_GROUPS="GROUPNAMES" # Comma seperated list of UNIX groups to add the users in 115 | USERADD_PROGRAM="/usr/sbin/useradd" # The useradd program to use. defaults to `/usr/sbin/useradd` 116 | USERADD_ARGS="--create-home --shell /bin/bash" # Arguments for the useradd program. defaults to `--create-home --shell /bin/bash` 117 | USERDEL_PROGRAM="/usr/sbin/userdel" # The userdel program to use. defaults to `/usr/sbin/userdel` 118 | USERDEL_ARGS="--force --remove" # Arguments for the userdel program. defaults to `--force --remove` 119 | ``` 120 | 121 | The LOCAL_MARKER_GROUP will be created if it does not exist. BEWARE: DO NOT add any manually created users 122 | to this group as they will be deleted in the next sync. This group is used by aws-ec2-ssh to keep track 123 | of what users were imported in the last run. 124 | 125 | ## Using a multi account strategy with a central IAM user account 126 | 127 | If you are using multiple AWS accounts you probably have one AWS account with all the IAM users (I will call it **users account**), and separate AWS accounts for your environments (I will call it **dev account**). Support for this is provided using the AssumeRole functionality in AWS. 128 | 129 | ### Setup users account 130 | 131 | 1. In the **users account**, create a new IAM role 132 | 2. Select Role Type **Role for Cross-Account Access** and select the option **Provide access between AWS accounts you own** 133 | 3. Put the **dev account** number in **Account ID** and leave **Require MFA** unchecked 134 | 4. Skip attaching a policy (we will do this soon) 135 | 5. Review the new role and create it 136 | 6. Select the newly created role 137 | 7. In the **Permissions** tab, expand **Inline Policies** and create a new inline policy 138 | 8. Select **Custom Policy** 139 | 9. Paste the content of the [`iam_ssh_policy.json`](./iam_ssh_policy.json) file and replace `` with the AWS Account ID of the **users account**. 140 | 141 | ### Setup dev account 142 | 143 | For your EC2 instances, you need a IAM role that allows the `sts:AssumeRole` action 144 | 145 | 1. In the **dev account**, create a new IAM role 146 | 2. Select ROle Type **AWS Service Roles** and select the option **Amazon EC2** 147 | 3. Skip attaching a policy (we will do this soon) 148 | 4. Review the new role and create it 149 | 5. Select the newly created role 150 | 6. In the **Permissions** tab, expand **Inline Policies** and create a new inline policy 151 | 7. Select **Custom Policy** 152 | 8. Paste the content of the [`iam_crossaccount_policy.json`](./iam_crossaccount_policy.json) file and replace `` with the AWS Account ID of the **users account** and `` with the IAM rol name that you created in the **users account** 153 | 9. Create/edit the file `/etc/aws-ec2-ssh.conf` and add this line: `ASSUMEROLE="IAM-ROLE-ARN` or run the install.sh script with the -a argument 154 | 155 | ## Limitations 156 | 157 | * your EC2 instances need access to the AWS API either via an Internet Gateway + public IP or a Nat Gatetway / instance. 158 | * it can take up to 10 minutes until a new IAM user can log in 159 | * if you delete the IAM user / ssh public key and the user is already logged in, the SSH session will not be closed 160 | * uid's and gid's across multiple servers might not line up correctly (due to when a server was booted, and what users existed at that time). Could affect NFS mounts or Amazon EFS. 161 | * this solution will work for ~100 IAM users and ~100 EC2 instances. If your setup is much larger (e.g. 10 times more users or 10 times more EC2 instances) you may run into two issues: 162 | * IAM API limitations 163 | * Disk space issues 164 | * **not all IAM user names are allowed in Linux user names** (e.g. if you use email addresses as IAM user names). See section [IAM user names and Linux user names](#iam-user-names-and-linux-user-names) for further details. 165 | -------------------------------------------------------------------------------- /import_users.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | function log() { 4 | /usr/bin/logger -i -p auth.info -t aws-ec2-ssh "$@" 5 | } 6 | 7 | # check if AWS CLI exists 8 | if ! [ -x "$(which aws)" ]; then 9 | log "aws executable not found - exiting!" 10 | exit 1 11 | fi 12 | 13 | # source configuration if it exists 14 | [ -f /etc/aws-ec2-ssh.conf ] && . /etc/aws-ec2-ssh.conf 15 | 16 | # Should we actually do something? 17 | : ${DONOTSYNC:=0} 18 | 19 | if [ ${DONOTSYNC} -eq 1 ] 20 | then 21 | log "Please configure aws-ec2-ssh by editing /etc/aws-ec2-ssh.conf" 22 | exit 1 23 | fi 24 | 25 | # Which IAM groups have access to this instance 26 | # Comma seperated list of IAM groups. Leave empty for all available IAM users 27 | : ${IAM_AUTHORIZED_GROUPS:=""} 28 | 29 | # Special group to mark users as being synced by our script 30 | : ${LOCAL_MARKER_GROUP:="iam-synced-users"} 31 | 32 | # Give the users these local UNIX groups 33 | : ${LOCAL_GROUPS:=""} 34 | 35 | # Specify an IAM group for users who should be given sudo privileges, or leave 36 | # empty to not change sudo access, or give it the value '##ALL##' to have all 37 | # users be given sudo rights. 38 | # DEPRECATED! Use SUDOERS_GROUPS 39 | : ${SUDOERSGROUP:=""} 40 | 41 | # Specify a comma seperated list of IAM groups for users who should be given sudo privileges. 42 | # Leave empty to not change sudo access, or give the value '##ALL## to have all users 43 | # be given sudo rights. 44 | : ${SUDOERS_GROUPS:="${SUDOERSGROUP}"} 45 | 46 | # Assume a role before contacting AWS IAM to get users and keys. 47 | # This can be used if you define your users in one AWS account, while the EC2 48 | # instance you use this script runs in another. 49 | : ${ASSUMEROLE:=""} 50 | 51 | # Possibility to provide a custom useradd program 52 | : ${USERADD_PROGRAM:="/usr/sbin/useradd"} 53 | 54 | # Possibility to provide custom useradd arguments 55 | : ${USERADD_ARGS:="--user-group --create-home --shell /bin/bash"} 56 | 57 | # Possibility to provide a custom userdel program 58 | : ${USERDEL_PROGRAM:="/usr/sbin/userdel"} 59 | 60 | # Possibility to provide custom userdel arguments 61 | : ${USERDEL_ARGS:="--force --remove"} 62 | 63 | # Initizalize INSTANCE variable 64 | METADATA_TOKEN=$(curl -s -X PUT -H 'x-aws-ec2-metadata-token-ttl-seconds: 60' http://169.254.169.254/latest/api/token) 65 | INSTANCE_ID=$(curl -s -H "x-aws-ec2-metadata-token: ${METADATA_TOKEN}" http://169.254.169.254/latest/meta-data/instance-id) 66 | REGION=$(curl -s -H "x-aws-ec2-metadata-token: ${METADATA_TOKEN}" http://169.254.169.254/latest/dynamic/instance-identity/document | grep region | awk -F\" '{print $4}') 67 | 68 | function setup_aws_credentials() { 69 | local stscredentials 70 | if [[ ! -z "${ASSUMEROLE}" ]] 71 | then 72 | stscredentials=$(aws sts assume-role \ 73 | --role-arn "${ASSUMEROLE}" \ 74 | --role-session-name something \ 75 | --query '[Credentials.SessionToken,Credentials.AccessKeyId,Credentials.SecretAccessKey]' \ 76 | --output text) 77 | 78 | AWS_ACCESS_KEY_ID=$(echo "${stscredentials}" | awk '{print $2}') 79 | AWS_SECRET_ACCESS_KEY=$(echo "${stscredentials}" | awk '{print $3}') 80 | AWS_SESSION_TOKEN=$(echo "${stscredentials}" | awk '{print $1}') 81 | AWS_SECURITY_TOKEN=$(echo "${stscredentials}" | awk '{print $1}') 82 | export AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_SECURITY_TOKEN 83 | fi 84 | } 85 | 86 | # Get list of iam groups from tag 87 | function get_iam_groups_from_tag() { 88 | if [ "${IAM_AUTHORIZED_GROUPS_TAG}" ] 89 | then 90 | IAM_AUTHORIZED_GROUPS=$(\ 91 | aws --region $REGION ec2 describe-tags \ 92 | --filters "Name=resource-id,Values=$INSTANCE_ID" "Name=key,Values=$IAM_AUTHORIZED_GROUPS_TAG" \ 93 | --query "Tags[0].Value" --output text \ 94 | ) 95 | fi 96 | } 97 | 98 | # Get all IAM users (optionally limited by IAM groups) 99 | function get_iam_users() { 100 | local group 101 | if [ -z "${IAM_AUTHORIZED_GROUPS}" ] 102 | then 103 | aws iam list-users \ 104 | --query "Users[].[UserName]" \ 105 | --output text \ 106 | | sed "s/\r//g" 107 | else 108 | for group in $(echo ${IAM_AUTHORIZED_GROUPS} | tr "," " "); do 109 | aws iam get-group \ 110 | --group-name "${group}" \ 111 | --query "Users[].[UserName]" \ 112 | --output text \ 113 | | sed "s/\r//g" 114 | done 115 | fi 116 | } 117 | 118 | # Run all found iam users through clean_iam_username 119 | function get_clean_iam_users() { 120 | local raw_username 121 | 122 | for raw_username in $(get_iam_users); do 123 | clean_iam_username "${raw_username}" | sed "s/\r//g" 124 | done 125 | } 126 | 127 | # Get previously synced users 128 | function get_local_users() { 129 | /usr/bin/getent group ${LOCAL_MARKER_GROUP} \ 130 | | cut -d : -f4- \ 131 | | sed "s/,/ /g" 132 | } 133 | 134 | # Get list of IAM groups marked with sudo access from tag 135 | function get_sudoers_groups_from_tag() { 136 | if [ "${SUDOERS_GROUPS_TAG}" ] 137 | then 138 | SUDOERS_GROUPS=$(\ 139 | aws --region $REGION ec2 describe-tags \ 140 | --filters "Name=resource-id,Values=$INSTANCE_ID" "Name=key,Values=$SUDOERS_GROUPS_TAG" \ 141 | --query "Tags[0].Value" --output text \ 142 | ) 143 | fi 144 | } 145 | 146 | # Get IAM users of the groups marked with sudo access 147 | function get_sudoers_users() { 148 | local group 149 | 150 | [[ -z "${SUDOERS_GROUPS}" ]] || [[ "${SUDOERS_GROUPS}" == "##ALL##" ]] || 151 | for group in $(echo "${SUDOERS_GROUPS}" | tr "," " "); do 152 | aws iam get-group \ 153 | --group-name "${group}" \ 154 | --query "Users[].[UserName]" \ 155 | --output text 156 | done 157 | } 158 | 159 | # Get the unix usernames of the IAM users within the sudo group 160 | function get_clean_sudoers_users() { 161 | local raw_username 162 | 163 | for raw_username in $(get_sudoers_users); do 164 | clean_iam_username "${raw_username}" 165 | done 166 | } 167 | 168 | # Create or update a local user based on info from the IAM group 169 | function create_or_update_local_user() { 170 | local username 171 | local sudousers 172 | local localusergroups 173 | 174 | username="${1}" 175 | sudousers="${2}" 176 | localusergroups="${LOCAL_MARKER_GROUP}" 177 | 178 | # check that username contains only alphanumeric, period (.), underscore (_), and hyphen (-) for a safe eval 179 | if [[ ! "${username}" =~ ^[0-9a-zA-Z\._\-]{1,32}$ ]] 180 | then 181 | log "Local user name ${username} contains illegal characters" 182 | exit 1 183 | fi 184 | 185 | if [ ! -z "${LOCAL_GROUPS}" ] 186 | then 187 | localusergroups="${LOCAL_GROUPS},${LOCAL_MARKER_GROUP}" 188 | fi 189 | 190 | if ! id "${username}" >/dev/null 2>&1; then 191 | ${USERADD_PROGRAM} ${USERADD_ARGS} "${username}" 192 | /bin/chown -R "${username}:${username}" "$(eval echo ~$username)" 193 | log "Created new user ${username}" 194 | fi 195 | /usr/sbin/usermod -a -G "${localusergroups}" "${username}" 196 | 197 | # Should we add this user to sudo ? 198 | if [[ ! -z "${SUDOERS_GROUPS}" ]] 199 | then 200 | SaveUserFileName=$(echo "${username}" | tr "." " ") 201 | SaveUserSudoFilePath="/etc/sudoers.d/$SaveUserFileName" 202 | if [[ "${SUDOERS_GROUPS}" == "##ALL##" ]] || echo "${sudousers}" | grep "^${username}\$" > /dev/null 203 | then 204 | echo "${username} ALL=(ALL) NOPASSWD:ALL" > "${SaveUserSudoFilePath}" 205 | else 206 | [[ ! -f "${SaveUserSudoFilePath}" ]] || rm "${SaveUserSudoFilePath}" 207 | fi 208 | fi 209 | } 210 | 211 | function delete_local_user() { 212 | # First, make sure no new sessions can be started 213 | /usr/sbin/usermod -L -s /sbin/nologin "${1}" || true 214 | # ask nicely and give them some time to shutdown 215 | /usr/bin/pkill -15 -u "${1}" || true 216 | sleep 5 217 | # Dont want to close nicely? DIE! 218 | /usr/bin/pkill -9 -u "${1}" || true 219 | sleep 1 220 | # Remove account now that all processes for the user are gone 221 | ${USERDEL_PROGRAM} ${USERDEL_ARGS} "${1}" 222 | 223 | log "Deleted user ${1}" 224 | } 225 | 226 | function clean_iam_username() { 227 | local clean_username="${1}" 228 | clean_username=${clean_username//"+"/".plus."} 229 | clean_username=${clean_username//"="/".equal."} 230 | clean_username=${clean_username//","/".comma."} 231 | clean_username=${clean_username//"@"/".at."} 232 | echo "${clean_username}" 233 | } 234 | 235 | function sync_accounts() { 236 | if [ -z "${LOCAL_MARKER_GROUP}" ] 237 | then 238 | log "Please specify a local group to mark imported users. eg iam-synced-users" 239 | exit 1 240 | fi 241 | 242 | # Check if local marker group exists, if not, create it 243 | /usr/bin/getent group "${LOCAL_MARKER_GROUP}" >/dev/null 2>&1 || /usr/sbin/groupadd "${LOCAL_MARKER_GROUP}" 244 | 245 | # declare and set some variables 246 | local iam_users 247 | local sudo_users 248 | local local_users 249 | local intersection 250 | local removed_users 251 | local user 252 | 253 | # init group and sudoers from tags 254 | get_iam_groups_from_tag 255 | get_sudoers_groups_from_tag 256 | 257 | # setup the aws credentials if needed 258 | setup_aws_credentials 259 | 260 | iam_users=$(get_clean_iam_users | sort | uniq) 261 | if [[ -z "${iam_users}" ]] 262 | then 263 | log "we just got back an empty iam_users user list which is likely caused by an IAM outage!" 264 | exit 1 265 | fi 266 | 267 | sudo_users=$(get_clean_sudoers_users | sort | uniq) 268 | if [[ ! -z "${SUDOERS_GROUPS}" ]] && [[ ! "${SUDOERS_GROUPS}" == "##ALL##" ]] && [[ -z "${sudo_users}" ]] 269 | then 270 | log "we just got back an empty sudo_users user list which is likely caused by an IAM outage!" 271 | exit 1 272 | fi 273 | 274 | local_users=$(get_local_users | sort | uniq) 275 | 276 | intersection=$(echo ${local_users} ${iam_users} | tr " " "\n" | sort | uniq -D | uniq) 277 | removed_users=$(echo ${local_users} ${intersection} | tr " " "\n" | sort | uniq -u) 278 | 279 | # Add or update the users found in IAM 280 | for user in ${iam_users}; do 281 | if [ "${#user}" -le "32" ] 282 | then 283 | create_or_update_local_user "${user}" "$sudo_users" 284 | else 285 | log "Can not import IAM user ${user}. User name is longer than 32 characters." 286 | fi 287 | done 288 | 289 | # Remove users no longer in the IAM group(s) 290 | for user in ${removed_users}; do 291 | delete_local_user "${user}" 292 | done 293 | } 294 | 295 | sync_accounts 296 | -------------------------------------------------------------------------------- /showcase-rpm.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # The MIT License (MIT) 3 | # 4 | # Copyright (c) 2016 widdix GmbH 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | # 8 | # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | # 10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | AWSTemplateFormatVersion: '2010-09-09' 12 | Description: 'AWS EC2 SSH access with IAM showcase using RPM' 13 | Parameters: 14 | VPC: 15 | Type: 'AWS::EC2::VPC::Id' 16 | Description: 'The VPC the EC2 instance is launched into.' 17 | Subnet: 18 | Type: 'AWS::EC2::Subnet::Id' 19 | Description: 'The subnet the EC2 instance is launched into.' 20 | AssumeRole: 21 | Type: 'String' 22 | Description: 'Optional IAM role ARN to assume to get the IAM users from another account' 23 | Default: '' 24 | KeyName: 25 | Description: 'Optional key pair of the ec2-user to establish a SSH connection to the EC2 instance when things go wrong.' 26 | Type: String 27 | Default: '' 28 | OS: 29 | Description: 'Operating system' 30 | Type: String 31 | Default: 'AmazonLinux' 32 | AllowedValues: 33 | - AmazonLinux 34 | - AmazonLinux2 35 | - SUSELinuxEnterpriseServer 36 | - RHEL 37 | - CentOS 38 | Version: 39 | Description: 'See https://github.com/widdix/aws-ec2-ssh/releases for available versions' 40 | Type: String 41 | Default: '1.6.0-1' 42 | Mappings: 43 | OSMap: 44 | AmazonLinux: 45 | RegionMap: RegionMapAmazonLinux 46 | UserData: | 47 | trap '/opt/aws/bin/cfn-signal -e 1 --stack=${STACKNAME} --region=${REGION} --resource=Instance' ERR 48 | /opt/aws/bin/cfn-init --verbose --stack=${STACKNAME} --region=${REGION} --resource=Instance 49 | /opt/aws/bin/cfn-signal -e 0 --stack=${STACKNAME} --region=${REGION} --resource=Instance 50 | AmazonLinux2: 51 | RegionMap: RegionMapAmazonLinux2 52 | UserData: | 53 | trap '/opt/aws/bin/cfn-signal -e 1 --stack=${STACKNAME} --region=${REGION} --resource=Instance' ERR 54 | /opt/aws/bin/cfn-init --verbose --stack=${STACKNAME} --region=${REGION} --resource=Instance 55 | /opt/aws/bin/cfn-signal -e 0 --stack=${STACKNAME} --region=${REGION} --resource=Instance 56 | SUSELinuxEnterpriseServer: 57 | RegionMap: RegionMapSUSELinuxEnterpriseServer 58 | UserData: | 59 | trap '/usr/bin/cfn-signal -e 1 --stack=${STACKNAME} --region=${REGION} --resource=Instance' ERR 60 | mkdir aws-cfn-bootstrap-latest 61 | curl -s -m 60 https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz | tar xz -C aws-cfn-bootstrap-latest --strip-components 1 62 | easy_install aws-cfn-bootstrap-latest 63 | /usr/bin/cfn-init --verbose --stack=${STACKNAME} --region=${REGION} --resource=Instance 64 | /usr/bin/cfn-signal -e 0 --stack=${STACKNAME} --region=${REGION} --resource=Instance 65 | RHEL: 66 | RegionMap: RegionMapRHEL 67 | UserData: | 68 | trap '/bin/cfn-signal -e 1 --stack=${STACKNAME} --region=${REGION} --resource=Instance' ERR 69 | mkdir aws-cfn-bootstrap-latest 70 | curl -s -m 60 https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz | tar xz -C aws-cfn-bootstrap-latest --strip-components 1 71 | easy_install aws-cfn-bootstrap-latest 72 | /bin/cfn-init --verbose --stack=${STACKNAME} --region=${REGION} --resource=Instance 73 | /bin/cfn-signal -e 0 --stack=${STACKNAME} --region=${REGION} --resource=Instance 74 | CentOS: 75 | RegionMap: RegionMapCentOS 76 | UserData: | 77 | trap '/bin/cfn-signal -e 1 --stack=${STACKNAME} --region=${REGION} --resource=Instance' ERR 78 | mkdir aws-cfn-bootstrap-latest 79 | curl -s -m 60 https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz | tar xz -C aws-cfn-bootstrap-latest --strip-components 1 80 | easy_install aws-cfn-bootstrap-latest 81 | /bin/cfn-init --verbose --stack=${STACKNAME} --region=${REGION} --resource=Instance 82 | /bin/cfn-signal -e 0 --stack=${STACKNAME} --region=${REGION} --resource=Instance 83 | RegionMapAmazonLinux: 84 | 'af-south-1': 85 | AMI: 'ami-0bd0ff98d5803baf2' 86 | 'eu-north-1': 87 | AMI: 'ami-03d14071fbfdb6345' 88 | 'ap-south-1': 89 | AMI: 'ami-031102d49fd792f54' 90 | 'eu-west-3': 91 | AMI: 'ami-0bf262dcebfb032fe' 92 | 'eu-west-2': 93 | AMI: 'ami-0ac220457a4ecc013' 94 | 'eu-south-1': 95 | AMI: 'ami-01c317f645a88f29e' 96 | 'eu-west-1': 97 | AMI: 'ami-09c3a622d80fc8fb5' 98 | 'ap-northeast-3': 99 | AMI: 'ami-008419a43a4dee425' 100 | 'ap-northeast-2': 101 | AMI: 'ami-059143f9ca10c94c4' 102 | 'me-south-1': 103 | AMI: 'ami-061126e80962772f4' 104 | 'ap-northeast-1': 105 | AMI: 'ami-0023b115b784081c7' 106 | 'sa-east-1': 107 | AMI: 'ami-0bc4690de6ff8e9da' 108 | 'ca-central-1': 109 | AMI: 'ami-0724329a0a9fbe54f' 110 | 'ap-east-1': 111 | AMI: 'ami-09178b7c189f29d09' 112 | 'ap-southeast-1': 113 | AMI: 'ami-0c6eb4da54c38416e' 114 | 'ap-southeast-2': 115 | AMI: 'ami-0641d8cd8757c7fbc' 116 | 'eu-central-1': 117 | AMI: 'ami-02d6b68c62d3a94f3' 118 | 'us-east-1': 119 | AMI: 'ami-0a2c275b42dee0b81' 120 | 'us-east-2': 121 | AMI: 'ami-0080a5472dff19765' 122 | 'us-west-1': 123 | AMI: 'ami-018656cf0fea785ca' 124 | 'us-west-2': 125 | AMI: 'ami-06ab85f6beff39c19' 126 | RegionMapAmazonLinux2: 127 | 'af-south-1': 128 | AMI: 'ami-070fc0373e6c22c97' 129 | 'eu-north-1': 130 | AMI: 'ami-001c5f3c0a8b3f245' 131 | 'ap-south-1': 132 | AMI: 'ami-041db4a969fe3eb68' 133 | 'eu-west-3': 134 | AMI: 'ami-0da7ba92c3c072475' 135 | 'eu-west-2': 136 | AMI: 'ami-074771aa49ab046e7' 137 | 'eu-south-1': 138 | AMI: 'ami-07f0f6adab1daa189' 139 | 'eu-west-1': 140 | AMI: 'ami-0ed961fa828560210' 141 | 'ap-northeast-3': 142 | AMI: 'ami-026107638e7d599b1' 143 | 'ap-northeast-2': 144 | AMI: 'ami-04e8dfc09b22389ad' 145 | 'me-south-1': 146 | AMI: 'ami-0980753c2dfad9136' 147 | 'ap-northeast-1': 148 | AMI: 'ami-02d36247c5bc58c23' 149 | 'sa-east-1': 150 | AMI: 'ami-07983613af1a3ef44' 151 | 'ca-central-1': 152 | AMI: 'ami-0d8ad3ab25e7abc51' 153 | 'ap-east-1': 154 | AMI: 'ami-0d41b14f11c68e677' 155 | 'ap-southeast-1': 156 | AMI: 'ami-07191cf2912e097a6' 157 | 'ap-southeast-2': 158 | AMI: 'ami-04a81599b183d7908' 159 | 'eu-central-1': 160 | AMI: 'ami-047e03b8591f2d48a' 161 | 'us-east-1': 162 | AMI: 'ami-01cc34ab2709337aa' 163 | 'us-east-2': 164 | AMI: 'ami-0f19d220602031aed' 165 | 'us-west-1': 166 | AMI: 'ami-0e0bf4b3a0c0e0adc' 167 | 'us-west-2': 168 | AMI: 'ami-0e5b6b6a9f3db6db8' 169 | RegionMapSUSELinuxEnterpriseServer: 170 | 'eu-north-1': 171 | AMI: 'ami-03391c07852c73dbe' 172 | 'ap-south-1': 173 | AMI: 'ami-0c1453eb750c7a5ab' 174 | 'eu-west-3': 175 | AMI: 'ami-07b20332d54ed21e8' 176 | 'eu-west-2': 177 | AMI: 'ami-0eb4bd78fba2f32e4' 178 | 'eu-west-1': 179 | AMI: 'ami-06bc7889ee68f279e' 180 | 'ap-northeast-3': 181 | AMI: 'ami-0b54f5c0822c3b02a' 182 | 'ap-northeast-2': 183 | AMI: 'ami-00728f974fb4b3f2a' 184 | 'ap-northeast-1': 185 | AMI: 'ami-050f5170e43607893' 186 | 'sa-east-1': 187 | AMI: 'ami-0188b2a9dc0ae5f44' 188 | 'ca-central-1': 189 | AMI: 'ami-0f7c9a39e20a9adea' 190 | 'ap-southeast-1': 191 | AMI: 'ami-06190570cf455031a' 192 | 'ap-southeast-2': 193 | AMI: 'ami-0ccbc8eb74e84b8bd' 194 | 'eu-central-1': 195 | AMI: 'ami-0fa9bde3f3d40e5ae' 196 | 'us-east-1': 197 | AMI: 'ami-03adb8813ffd80f0b' 198 | 'us-east-2': 199 | AMI: 'ami-0479b39f2d07530fb' 200 | 'us-west-1': 201 | AMI: 'ami-05561250b8346a707' 202 | 'us-west-2': 203 | AMI: 'ami-015ee9a0398544b09' 204 | RegionMapRHEL: 205 | 'ap-south-1': 206 | AMI: 'ami-e41b618b' 207 | 'eu-west-3': 208 | AMI: 'ami-39902744' 209 | 'eu-west-2': 210 | AMI: 'ami-a1f5e4c5' 211 | 'eu-west-1': 212 | AMI: 'ami-bb9a6bc2' 213 | 'ap-northeast-2': 214 | AMI: 'ami-0f5a8361' 215 | 'ap-northeast-1': 216 | AMI: 'ami-30ef0556' 217 | 'sa-east-1': 218 | AMI: 'ami-a789ffcb' 219 | 'ca-central-1': 220 | AMI: 'ami-dad866be' 221 | 'ap-southeast-1': 222 | AMI: 'ami-10bb2373' 223 | 'ap-southeast-2': 224 | AMI: 'ami-ccecf5af' 225 | 'eu-central-1': 226 | AMI: 'ami-d74be5b8' 227 | 'us-east-1': 228 | AMI: 'ami-c998b6b2' 229 | 'us-east-2': 230 | AMI: 'ami-cfdafaaa' 231 | 'us-west-1': 232 | AMI: 'ami-66eec506' 233 | 'us-west-2': 234 | AMI: 'ami-9fa343e7' 235 | RegionMapCentOS: 236 | 'af-south-1': 237 | AMI: 'ami-0b761332115c38669' 238 | 'eu-north-1': 239 | AMI: 'ami-0358414bac2039369' 240 | 'ap-south-1': 241 | AMI: 'ami-0ffc7af9c06de0077' 242 | 'eu-west-3': 243 | AMI: 'ami-072ec828dae86abe5' 244 | 'eu-west-2': 245 | AMI: 'ami-0b22fcaf3564fb0c9' 246 | 'eu-south-1': 247 | AMI: 'ami-0fe3899b62205176a' 248 | 'eu-west-1': 249 | AMI: 'ami-04f5641b0d178a27a' 250 | 'ap-northeast-2': 251 | AMI: 'ami-0e4214f08b51e23cc' 252 | 'me-south-1': 253 | AMI: 'ami-0ac17dcdd6f6f4eb6' 254 | 'ap-northeast-1': 255 | AMI: 'ami-0ddea5e0f69c193a4' 256 | 'sa-east-1': 257 | AMI: 'ami-02334c45dd95ca1fc' 258 | 'ca-central-1': 259 | AMI: 'ami-0a7c5b189b6460115' 260 | 'ap-east-1': 261 | AMI: 'ami-09611bd6fa5dd0e3d' 262 | 'ap-southeast-1': 263 | AMI: 'ami-0adfdaea54d40922b' 264 | 'ap-southeast-2': 265 | AMI: 'ami-03d56f451ca110e99' 266 | 'eu-central-1': 267 | AMI: 'ami-08b6d44b4f6f7b279' 268 | 'us-east-1': 269 | AMI: 'ami-00e87074e52e6c9f9' 270 | 'us-east-2': 271 | AMI: 'ami-00f8e2c955f7ffa9b' 272 | 'us-west-1': 273 | AMI: 'ami-08d2d8b00f270d03b' 274 | 'us-west-2': 275 | AMI: 'ami-0686851c4e7b1a8e1' 276 | Conditions: 277 | UseCrossAccountIAM: !Not [!Equals [!Ref AssumeRole, '']] 278 | UseLocalIAM: !Equals [!Ref AssumeRole, ''] 279 | HasKeyName: !Not [!Equals [!Ref KeyName, '']] 280 | Resources: 281 | SecurityGroup: 282 | Type: 'AWS::EC2::SecurityGroup' 283 | Properties: 284 | GroupDescription: ssh 285 | VpcId: !Ref VPC 286 | SecurityGroupIngress: 287 | - CidrIp: '0.0.0.0/0' 288 | IpProtocol: tcp 289 | FromPort: 22 290 | ToPort: 22 291 | InstanceProfile: 292 | Type: 'AWS::IAM::InstanceProfile' 293 | Properties: 294 | Roles: 295 | - !Ref Role 296 | Role: 297 | Type: 'AWS::IAM::Role' 298 | Properties: 299 | AssumeRolePolicyDocument: 300 | Version: '2012-10-17' 301 | Statement: 302 | - Effect: Allow 303 | Principal: 304 | Service: 'ec2.amazonaws.com' 305 | Action: 'sts:AssumeRole' 306 | Path: / 307 | CrossAccountRolePolicy: 308 | Type: 'AWS::IAM::Policy' 309 | Condition: UseCrossAccountIAM 310 | Properties: 311 | PolicyName: crossaccountiam 312 | PolicyDocument: 313 | Version: '2012-10-17' 314 | Statement: 315 | - Effect: Allow 316 | Action: 'sts:AssumeRole' 317 | Resource: !Ref AssumeRole 318 | Roles: 319 | - !Ref Role 320 | LocalRolePolicy: 321 | Type: 'AWS::IAM::Policy' 322 | Condition: UseLocalIAM 323 | Properties: 324 | PolicyName: iam 325 | PolicyDocument: 326 | Version: '2012-10-17' 327 | Statement: 328 | - Effect: Allow 329 | Action: 330 | - 'iam:ListUsers' 331 | - 'iam:GetGroup' 332 | Resource: '*' 333 | - Effect: Allow 334 | Action: 335 | - 'iam:ListSSHPublicKeys' 336 | - 'iam:GetSSHPublicKey' 337 | Resource: !Sub 'arn:aws:iam::${AWS::AccountId}:user/*' 338 | - Effect: Allow 339 | Action: 'ec2:DescribeTags' 340 | Resource: '*' 341 | Roles: 342 | - !Ref Role 343 | Instance: 344 | Type: AWS::EC2::Instance 345 | Metadata: 346 | 'AWS::CloudFormation::Init': 347 | configSets: 348 | default: [!Sub 'prepare${OS}', install] 349 | prepareAmazonLinux: 350 | packages: 351 | yum: 352 | git: [] 353 | prepareAmazonLinux2: 354 | packages: 355 | yum: 356 | git: [] 357 | prepareSUSELinuxEnterpriseServer: {} 358 | prepareRHEL: 359 | packages: 360 | yum: 361 | git: [] 362 | unzip: [] 363 | commands: 364 | a_download: 365 | command: 'curl "https://s3.amazonaws.com/aws-cli/awscli-bundle.zip" -o "awscli-bundle.zip"' 366 | cwd: '/tmp' 367 | b_extract: 368 | command: 'unzip awscli-bundle.zip' 369 | cwd: '/tmp' 370 | c_install: 371 | command: './awscli-bundle/install -i /usr/local/aws -b /bin/aws' 372 | cwd: '/tmp' 373 | prepareCentOS: 374 | packages: 375 | rpm: 376 | epel: 'http://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm' 377 | yum: 378 | git: [] 379 | awscli: [] 380 | install: 381 | packages: 382 | rpm: 383 | aws-ec2-ssh: !Sub 'https://s3-eu-west-1.amazonaws.com/widdix-aws-ec2-ssh-releases-eu-west-1/aws-ec2-ssh-${Version}.el7.centos.noarch.rpm' 384 | commands: 385 | a_enable: 386 | command: "sed -i 's/DONOTSYNC=1/DONOTSYNC=0/g' /etc/aws-ec2-ssh.conf && /usr/bin/import_users.sh" 387 | test: "grep -q 'DONOTSYNC=1' /etc/aws-ec2-ssh.conf" 388 | Properties: 389 | ImageId: !FindInMap [!FindInMap [OSMap, !Ref OS, RegionMap], !Ref 'AWS::Region', AMI] 390 | IamInstanceProfile: !Ref InstanceProfile 391 | InstanceType: 't2.micro' 392 | KeyName: !If [HasKeyName, !Ref KeyName, !Ref 'AWS::NoValue'] 393 | UserData: 394 | 'Fn::Base64': !Sub 395 | - | 396 | #!/bin/bash -ex 397 | export REGION=${AWS::Region} 398 | export STACKNAME=${AWS::StackName} 399 | ${UserData} 400 | - UserData: !FindInMap [OSMap, !Ref OS, UserData] 401 | NetworkInterfaces: 402 | - AssociatePublicIpAddress: true 403 | DeleteOnTermination: true 404 | SubnetId: !Ref Subnet 405 | DeviceIndex: 0 406 | GroupSet: 407 | - !Ref SecurityGroup 408 | Tags: 409 | - Key: Name 410 | Value: 'AWS EC2 SSH access with IAM showcase' 411 | CreationPolicy: 412 | ResourceSignal: 413 | Count: 1 414 | Timeout: PT20M 415 | Outputs: 416 | PublicName: 417 | Description: 'The public name of the EC2 instance.' 418 | Value: !GetAtt 'Instance.PublicDnsName' 419 | -------------------------------------------------------------------------------- /showcase.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # The MIT License (MIT) 3 | # 4 | # Copyright (c) 2016 widdix GmbH 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | # 8 | # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | # 10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | AWSTemplateFormatVersion: '2010-09-09' 12 | Description: 'AWS EC2 SSH access with IAM showcase using install.sh' 13 | Parameters: 14 | VPC: 15 | Type: 'AWS::EC2::VPC::Id' 16 | Description: 'The VPC the EC2 instance is launched into.' 17 | Subnet: 18 | Type: 'AWS::EC2::Subnet::Id' 19 | Description: 'The subnet the EC2 instance is launched into.' 20 | AssumeRole: 21 | Type: 'String' 22 | Description: 'Optional IAM role ARN to assume to get the IAM users from another account' 23 | Default: '' 24 | KeyName: 25 | Description: 'Optional key pair of the ec2-user to establish a SSH connection to the EC2 instance when things go wrong.' 26 | Type: String 27 | Default: '' 28 | OS: 29 | Description: 'Operating system' 30 | Type: String 31 | Default: 'AmazonLinux' 32 | AllowedValues: 33 | - AmazonLinux 34 | - AmazonLinux2 35 | - Ubuntu 36 | - SUSELinuxEnterpriseServer 37 | - RHEL 38 | - CentOS 39 | Mappings: 40 | OSMap: 41 | AmazonLinux: 42 | RegionMap: RegionMapAmazonLinux 43 | UserData: | 44 | trap '/opt/aws/bin/cfn-signal -e 1 --stack=${STACKNAME} --region=${REGION} --resource=Instance' ERR 45 | /opt/aws/bin/cfn-init --verbose --stack=${STACKNAME} --region=${REGION} --resource=Instance 46 | /opt/aws/bin/cfn-signal -e 0 --stack=${STACKNAME} --region=${REGION} --resource=Instance 47 | AmazonLinux2: 48 | RegionMap: RegionMapAmazonLinux2 49 | UserData: | 50 | trap '/opt/aws/bin/cfn-signal -e 1 --stack=${STACKNAME} --region=${REGION} --resource=Instance' ERR 51 | /opt/aws/bin/cfn-init --verbose --stack=${STACKNAME} --region=${REGION} --resource=Instance 52 | /opt/aws/bin/cfn-signal -e 0 --stack=${STACKNAME} --region=${REGION} --resource=Instance 53 | Ubuntu: 54 | RegionMap: RegionMapUbuntu 55 | UserData: | 56 | trap '/usr/local/bin/cfn-signal -e 1 --stack=${STACKNAME} --region=${REGION} --resource=Instance' ERR 57 | apt-get update 58 | apt-get -y install python-setuptools 59 | mkdir aws-cfn-bootstrap-latest 60 | curl -s -m 60 https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz | tar xz -C aws-cfn-bootstrap-latest --strip-components 1 61 | easy_install aws-cfn-bootstrap-latest 62 | /usr/local/bin/cfn-init --verbose --stack=${STACKNAME} --region=${REGION} --resource=Instance 63 | /usr/local/bin/cfn-signal -e 0 --stack=${STACKNAME} --region=${REGION} --resource=Instance 64 | SUSELinuxEnterpriseServer: 65 | RegionMap: RegionMapSUSELinuxEnterpriseServer 66 | UserData: | 67 | trap '/usr/bin/cfn-signal -e 1 --stack=${STACKNAME} --region=${REGION} --resource=Instance' ERR 68 | mkdir aws-cfn-bootstrap-latest 69 | curl -s -m 60 https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz | tar xz -C aws-cfn-bootstrap-latest --strip-components 1 70 | easy_install aws-cfn-bootstrap-latest 71 | /usr/bin/cfn-init --verbose --stack=${STACKNAME} --region=${REGION} --resource=Instance 72 | /usr/bin/cfn-signal -e 0 --stack=${STACKNAME} --region=${REGION} --resource=Instance 73 | RHEL: 74 | RegionMap: RegionMapRHEL 75 | UserData: | 76 | trap '/bin/cfn-signal -e 1 --stack=${STACKNAME} --region=${REGION} --resource=Instance' ERR 77 | mkdir aws-cfn-bootstrap-latest 78 | curl -s -m 60 https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz | tar xz -C aws-cfn-bootstrap-latest --strip-components 1 79 | easy_install aws-cfn-bootstrap-latest 80 | /bin/cfn-init --verbose --stack=${STACKNAME} --region=${REGION} --resource=Instance 81 | /bin/cfn-signal -e 0 --stack=${STACKNAME} --region=${REGION} --resource=Instance 82 | CentOS: 83 | RegionMap: RegionMapCentOS 84 | UserData: | 85 | trap '/bin/cfn-signal -e 1 --stack=${STACKNAME} --region=${REGION} --resource=Instance' ERR 86 | mkdir aws-cfn-bootstrap-latest 87 | curl -s -m 60 https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz | tar xz -C aws-cfn-bootstrap-latest --strip-components 1 88 | easy_install aws-cfn-bootstrap-latest 89 | /bin/cfn-init --verbose --stack=${STACKNAME} --region=${REGION} --resource=Instance 90 | /bin/cfn-signal -e 0 --stack=${STACKNAME} --region=${REGION} --resource=Instance 91 | RegionMapAmazonLinux: 92 | 'af-south-1': 93 | AMI: 'ami-0bd0ff98d5803baf2' 94 | 'eu-north-1': 95 | AMI: 'ami-03d14071fbfdb6345' 96 | 'ap-south-1': 97 | AMI: 'ami-031102d49fd792f54' 98 | 'eu-west-3': 99 | AMI: 'ami-0bf262dcebfb032fe' 100 | 'eu-west-2': 101 | AMI: 'ami-0ac220457a4ecc013' 102 | 'eu-south-1': 103 | AMI: 'ami-01c317f645a88f29e' 104 | 'eu-west-1': 105 | AMI: 'ami-09c3a622d80fc8fb5' 106 | 'ap-northeast-3': 107 | AMI: 'ami-008419a43a4dee425' 108 | 'ap-northeast-2': 109 | AMI: 'ami-059143f9ca10c94c4' 110 | 'me-south-1': 111 | AMI: 'ami-061126e80962772f4' 112 | 'ap-northeast-1': 113 | AMI: 'ami-0023b115b784081c7' 114 | 'sa-east-1': 115 | AMI: 'ami-0bc4690de6ff8e9da' 116 | 'ca-central-1': 117 | AMI: 'ami-0724329a0a9fbe54f' 118 | 'ap-east-1': 119 | AMI: 'ami-09178b7c189f29d09' 120 | 'ap-southeast-1': 121 | AMI: 'ami-0c6eb4da54c38416e' 122 | 'ap-southeast-2': 123 | AMI: 'ami-0641d8cd8757c7fbc' 124 | 'eu-central-1': 125 | AMI: 'ami-02d6b68c62d3a94f3' 126 | 'us-east-1': 127 | AMI: 'ami-0a2c275b42dee0b81' 128 | 'us-east-2': 129 | AMI: 'ami-0080a5472dff19765' 130 | 'us-west-1': 131 | AMI: 'ami-018656cf0fea785ca' 132 | 'us-west-2': 133 | AMI: 'ami-06ab85f6beff39c19' 134 | RegionMapAmazonLinux2: 135 | 'af-south-1': 136 | AMI: 'ami-070fc0373e6c22c97' 137 | 'eu-north-1': 138 | AMI: 'ami-001c5f3c0a8b3f245' 139 | 'ap-south-1': 140 | AMI: 'ami-041db4a969fe3eb68' 141 | 'eu-west-3': 142 | AMI: 'ami-0da7ba92c3c072475' 143 | 'eu-west-2': 144 | AMI: 'ami-074771aa49ab046e7' 145 | 'eu-south-1': 146 | AMI: 'ami-07f0f6adab1daa189' 147 | 'eu-west-1': 148 | AMI: 'ami-0ed961fa828560210' 149 | 'ap-northeast-3': 150 | AMI: 'ami-026107638e7d599b1' 151 | 'ap-northeast-2': 152 | AMI: 'ami-04e8dfc09b22389ad' 153 | 'me-south-1': 154 | AMI: 'ami-0980753c2dfad9136' 155 | 'ap-northeast-1': 156 | AMI: 'ami-02d36247c5bc58c23' 157 | 'sa-east-1': 158 | AMI: 'ami-07983613af1a3ef44' 159 | 'ca-central-1': 160 | AMI: 'ami-0d8ad3ab25e7abc51' 161 | 'ap-east-1': 162 | AMI: 'ami-0d41b14f11c68e677' 163 | 'ap-southeast-1': 164 | AMI: 'ami-07191cf2912e097a6' 165 | 'ap-southeast-2': 166 | AMI: 'ami-04a81599b183d7908' 167 | 'eu-central-1': 168 | AMI: 'ami-047e03b8591f2d48a' 169 | 'us-east-1': 170 | AMI: 'ami-01cc34ab2709337aa' 171 | 'us-east-2': 172 | AMI: 'ami-0f19d220602031aed' 173 | 'us-west-1': 174 | AMI: 'ami-0e0bf4b3a0c0e0adc' 175 | 'us-west-2': 176 | AMI: 'ami-0e5b6b6a9f3db6db8' 177 | RegionMapUbuntu: 178 | 'af-south-1': 179 | AMI: 'ami-063a9ea2ff5685f7f' 180 | 'eu-north-1': 181 | AMI: 'ami-000e50175c5f86214' 182 | 'ap-south-1': 183 | AMI: 'ami-0f2e255ec956ade7f' 184 | 'eu-west-3': 185 | AMI: 'ami-052f10f1c45aa2155' 186 | 'eu-west-2': 187 | AMI: 'ami-09a2a0f7d2db8baca' 188 | 'eu-south-1': 189 | AMI: 'ami-027f7881d2f6725e1' 190 | 'eu-west-1': 191 | AMI: 'ami-0f29c8402f8cce65c' 192 | 'ap-northeast-3': 193 | AMI: 'ami-00f5d213b513f1b07' 194 | 'ap-northeast-2': 195 | AMI: 'ami-0dd97ebb907cf9366' 196 | 'me-south-1': 197 | AMI: 'ami-0c41538a47f4b7d47' 198 | 'ap-northeast-1': 199 | AMI: 'ami-0822295a729d2a28e' 200 | 'sa-east-1': 201 | AMI: 'ami-0a729bdc1acf7528b' 202 | 'ca-central-1': 203 | AMI: 'ami-03bcd79f25ca6b127' 204 | 'ap-east-1': 205 | AMI: 'ami-038ff3475cbb62351' 206 | 'ap-southeast-1': 207 | AMI: 'ami-0f74c08b8b5effa56' 208 | 'ap-southeast-2': 209 | AMI: 'ami-0672b175139a0f8f4' 210 | 'eu-central-1': 211 | AMI: 'ami-09042b2f6d07d164a' 212 | 'us-east-1': 213 | AMI: 'ami-0b0ea68c435eb488d' 214 | 'us-east-2': 215 | AMI: 'ami-05803413c51f242b7' 216 | 'us-west-1': 217 | AMI: 'ami-0454207e5367abf01' 218 | 'us-west-2': 219 | AMI: 'ami-0688ba7eeeeefe3cd' 220 | RegionMapSUSELinuxEnterpriseServer: 221 | 'eu-north-1': 222 | AMI: 'ami-03391c07852c73dbe' 223 | 'ap-south-1': 224 | AMI: 'ami-0c1453eb750c7a5ab' 225 | 'eu-west-3': 226 | AMI: 'ami-07b20332d54ed21e8' 227 | 'eu-west-2': 228 | AMI: 'ami-0eb4bd78fba2f32e4' 229 | 'eu-west-1': 230 | AMI: 'ami-06bc7889ee68f279e' 231 | 'ap-northeast-3': 232 | AMI: 'ami-0b54f5c0822c3b02a' 233 | 'ap-northeast-2': 234 | AMI: 'ami-00728f974fb4b3f2a' 235 | 'ap-northeast-1': 236 | AMI: 'ami-050f5170e43607893' 237 | 'sa-east-1': 238 | AMI: 'ami-0188b2a9dc0ae5f44' 239 | 'ca-central-1': 240 | AMI: 'ami-0f7c9a39e20a9adea' 241 | 'ap-southeast-1': 242 | AMI: 'ami-06190570cf455031a' 243 | 'ap-southeast-2': 244 | AMI: 'ami-0ccbc8eb74e84b8bd' 245 | 'eu-central-1': 246 | AMI: 'ami-0fa9bde3f3d40e5ae' 247 | 'us-east-1': 248 | AMI: 'ami-03adb8813ffd80f0b' 249 | 'us-east-2': 250 | AMI: 'ami-0479b39f2d07530fb' 251 | 'us-west-1': 252 | AMI: 'ami-05561250b8346a707' 253 | 'us-west-2': 254 | AMI: 'ami-015ee9a0398544b09' 255 | RegionMapRHEL: 256 | 'ap-south-1': 257 | AMI: 'ami-e41b618b' 258 | 'eu-west-3': 259 | AMI: 'ami-39902744' 260 | 'eu-west-2': 261 | AMI: 'ami-a1f5e4c5' 262 | 'eu-west-1': 263 | AMI: 'ami-bb9a6bc2' 264 | 'ap-northeast-2': 265 | AMI: 'ami-0f5a8361' 266 | 'ap-northeast-1': 267 | AMI: 'ami-30ef0556' 268 | 'sa-east-1': 269 | AMI: 'ami-a789ffcb' 270 | 'ca-central-1': 271 | AMI: 'ami-dad866be' 272 | 'ap-southeast-1': 273 | AMI: 'ami-10bb2373' 274 | 'ap-southeast-2': 275 | AMI: 'ami-ccecf5af' 276 | 'eu-central-1': 277 | AMI: 'ami-d74be5b8' 278 | 'us-east-1': 279 | AMI: 'ami-c998b6b2' 280 | 'us-east-2': 281 | AMI: 'ami-cfdafaaa' 282 | 'us-west-1': 283 | AMI: 'ami-66eec506' 284 | 'us-west-2': 285 | AMI: 'ami-9fa343e7' 286 | RegionMapCentOS: 287 | 'af-south-1': 288 | AMI: 'ami-0b761332115c38669' 289 | 'eu-north-1': 290 | AMI: 'ami-0358414bac2039369' 291 | 'ap-south-1': 292 | AMI: 'ami-0ffc7af9c06de0077' 293 | 'eu-west-3': 294 | AMI: 'ami-072ec828dae86abe5' 295 | 'eu-west-2': 296 | AMI: 'ami-0b22fcaf3564fb0c9' 297 | 'eu-south-1': 298 | AMI: 'ami-0fe3899b62205176a' 299 | 'eu-west-1': 300 | AMI: 'ami-04f5641b0d178a27a' 301 | 'ap-northeast-2': 302 | AMI: 'ami-0e4214f08b51e23cc' 303 | 'me-south-1': 304 | AMI: 'ami-0ac17dcdd6f6f4eb6' 305 | 'ap-northeast-1': 306 | AMI: 'ami-0ddea5e0f69c193a4' 307 | 'sa-east-1': 308 | AMI: 'ami-02334c45dd95ca1fc' 309 | 'ca-central-1': 310 | AMI: 'ami-0a7c5b189b6460115' 311 | 'ap-east-1': 312 | AMI: 'ami-09611bd6fa5dd0e3d' 313 | 'ap-southeast-1': 314 | AMI: 'ami-0adfdaea54d40922b' 315 | 'ap-southeast-2': 316 | AMI: 'ami-03d56f451ca110e99' 317 | 'eu-central-1': 318 | AMI: 'ami-08b6d44b4f6f7b279' 319 | 'us-east-1': 320 | AMI: 'ami-00e87074e52e6c9f9' 321 | 'us-east-2': 322 | AMI: 'ami-00f8e2c955f7ffa9b' 323 | 'us-west-1': 324 | AMI: 'ami-08d2d8b00f270d03b' 325 | 'us-west-2': 326 | AMI: 'ami-0686851c4e7b1a8e1' 327 | Conditions: 328 | UseCrossAccountIAM: !Not [!Equals [!Ref AssumeRole, '']] 329 | UseLocalIAM: !Equals [!Ref AssumeRole, ''] 330 | HasKeyName: !Not [!Equals [!Ref KeyName, '']] 331 | Resources: 332 | SecurityGroup: 333 | Type: 'AWS::EC2::SecurityGroup' 334 | Properties: 335 | GroupDescription: ssh 336 | VpcId: !Ref VPC 337 | SecurityGroupIngress: 338 | - CidrIp: '0.0.0.0/0' 339 | IpProtocol: tcp 340 | FromPort: 22 341 | ToPort: 22 342 | InstanceProfile: 343 | Type: 'AWS::IAM::InstanceProfile' 344 | Properties: 345 | Roles: 346 | - !Ref Role 347 | Role: 348 | Type: 'AWS::IAM::Role' 349 | Properties: 350 | AssumeRolePolicyDocument: 351 | Version: '2012-10-17' 352 | Statement: 353 | - Effect: Allow 354 | Principal: 355 | Service: 'ec2.amazonaws.com' 356 | Action: 'sts:AssumeRole' 357 | Path: / 358 | CrossAccountRolePolicy: 359 | Type: 'AWS::IAM::Policy' 360 | Condition: UseCrossAccountIAM 361 | Properties: 362 | PolicyName: crossaccountiam 363 | PolicyDocument: 364 | Version: '2012-10-17' 365 | Statement: 366 | - Effect: Allow 367 | Action: 'sts:AssumeRole' 368 | Resource: !Ref AssumeRole 369 | Roles: 370 | - !Ref Role 371 | LocalRolePolicy: 372 | Type: 'AWS::IAM::Policy' 373 | Condition: UseLocalIAM 374 | Properties: 375 | PolicyName: iam 376 | PolicyDocument: 377 | Version: '2012-10-17' 378 | Statement: 379 | - Effect: Allow 380 | Action: 381 | - 'iam:ListUsers' 382 | - 'iam:GetGroup' 383 | Resource: '*' 384 | - Effect: Allow 385 | Action: 386 | - 'iam:ListSSHPublicKeys' 387 | - 'iam:GetSSHPublicKey' 388 | Resource: !Sub 'arn:aws:iam::${AWS::AccountId}:user/*' 389 | - Effect: Allow 390 | Action: 'ec2:DescribeTags' 391 | Resource: '*' 392 | Roles: 393 | - !Ref Role 394 | Instance: 395 | Type: AWS::EC2::Instance 396 | Metadata: 397 | 'AWS::CloudFormation::Init': 398 | configSets: 399 | default: [!Sub 'prepare${OS}', install] 400 | prepareAmazonLinux: 401 | packages: 402 | yum: 403 | git: [] 404 | prepareAmazonLinux2: 405 | packages: 406 | yum: 407 | git: [] 408 | prepareUbuntu: 409 | packages: 410 | apt: 411 | git: [] 412 | awscli: [] 413 | prepareSUSELinuxEnterpriseServer: {} 414 | prepareRHEL: 415 | packages: 416 | yum: 417 | git: [] 418 | unzip: [] 419 | commands: 420 | a_download: 421 | command: 'curl "https://s3.amazonaws.com/aws-cli/awscli-bundle.zip" -o "awscli-bundle.zip"' 422 | cwd: '/tmp' 423 | b_extract: 424 | command: 'unzip awscli-bundle.zip' 425 | cwd: '/tmp' 426 | c_install: 427 | command: './awscli-bundle/install -i /usr/local/aws -b /bin/aws' 428 | cwd: '/tmp' 429 | prepareCentOS: 430 | packages: 431 | rpm: 432 | epel: 'http://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm' 433 | yum: 434 | git: [] 435 | awscli: [] 436 | install: 437 | files: 438 | '/opt/install.sh': 439 | source: 'https://raw.githubusercontent.com/widdix/aws-ec2-ssh/master/install.sh' 440 | mode: '000755' 441 | owner: root 442 | group: root 443 | commands: 444 | a_install: 445 | command: !Sub './install.sh -a "${AssumeRole}"' 446 | cwd: '/opt' 447 | Properties: 448 | ImageId: !FindInMap [!FindInMap [OSMap, !Ref OS, RegionMap], !Ref 'AWS::Region', AMI] 449 | IamInstanceProfile: !Ref InstanceProfile 450 | InstanceType: 't2.micro' 451 | KeyName: !If [HasKeyName, !Ref KeyName, !Ref 'AWS::NoValue'] 452 | UserData: 453 | 'Fn::Base64': !Sub 454 | - | 455 | #!/bin/bash -ex 456 | export REGION=${AWS::Region} 457 | export STACKNAME=${AWS::StackName} 458 | ${UserData} 459 | - UserData: !FindInMap [OSMap, !Ref OS, UserData] 460 | NetworkInterfaces: 461 | - AssociatePublicIpAddress: true 462 | DeleteOnTermination: true 463 | SubnetId: !Ref Subnet 464 | DeviceIndex: 0 465 | GroupSet: 466 | - !Ref SecurityGroup 467 | Tags: 468 | - Key: Name 469 | Value: 'AWS EC2 SSH access with IAM showcase' 470 | CreationPolicy: 471 | ResourceSignal: 472 | Count: 1 473 | Timeout: PT20M 474 | Outputs: 475 | PublicName: 476 | Description: 'The public name of the EC2 instance.' 477 | Value: !GetAtt 'Instance.PublicDnsName' 478 | --------------------------------------------------------------------------------