Ein Subnetz in AWS segmentiert einen IP-Adressbereich innerhalb eines VPCs. So ein Subnetz kann entweder privat oder öffentlich mit einem Gateway zum Internet sein. Instanzen, die sich in einem öffentlichen Subnetz befinden, können ausgehenden Datenverkehr in das Internet senden sowie eingehenden Datenverkehr empfangen. Auf der anderen Seite können Instanzen in einem privaten Subnetz ausgehenden Datenverkehr nur über ein NAT-Gateway in einem öffentlichen Subnetz senden.

Die Verwendung von privaten Subnetzen ist sicherer, da EC2-Instanzen in diesen nicht aus dem Internet erreichbar sind. Private Subnetze bieten zudem den Vorteil, dass der Datenverkehr zwischen zwei Kommunikationspartnern in diesem Netz privat und steuerbar bleibt. Dies verkleinert die Angriffsfläche und führt zu mehr Datensicherheit. Würden wir unsere CouchDB nach EC2 migrieren, so würden sich diese EC2-Instanzen auf jeden Fall ausschließlich im privaten Subnetz befinden.

Wenn sich die Instanzen nun aber in einem privaten Netz befinden, stellt sich die Frage, wie man von seinem lokalen Rechner per SSH auf die Maschinen zugreifen kann.

Verbindung via Bastion Host

Eine sehr bekannte Lösung ist hier die Verwendung eines Bastion Hosts oder Jump Hosts. Der Bastion Host befindet sich im öffentlichen Subnetz und damit im Internet. Er ist dadurch von der lokalen Maschine aus via SSH erreichbar, wenn die angehängte Security Group dies erlaubt. Über den Bastion Host ist eine Verbindung zur privaten EC2-Instanz möglich, wenn sich Bastion Host und private Instanz im selben VPC befinden.

Die Verwendung eines Bastion Hosts stellt ein Sicherheitsrisiko dar, da er mit geöffnetem Port 22 aus dem Internet für jedermann erreichbar ist. Er muss daher in jedem Fall besonders gehärtet sein und die regelmäßige Installation von Patches muss sichergestellt werden, damit der Host sicher bleibt. Zudem werden Logins auf einem Bastion Host nicht in Cloud Trail protokolliert, was die Nachverfolgbarkeit bei z.B. Angriffen erschweren kann. Folgende Abbildung veranschaulicht das skizzierte Setup:

Connect to private ec2 instance via bastion host

Verbindung via AWS Session Manager

Der AWS Session Manager stellt eine weitaus sichererer Alternativ zum Bastion Host dar, der die im letzten Abschnitt genannten Sicherheitsrisiken mitigiert.

Gegenüber der Verwendung eines Bastion Hosts ergeben sich die folgenden Vorteile:

  • Es müssen keine Ports für eingehenden Datenverkehr geöffnet werden, d.h. auch Port 22 muss nicht geöffnet werden.
  • Der AWS Session Manager kommuniziert mit den Instanzen über einen SSM Agent über einen verschlüsselten Tunnel, der seinen Ursprung auf der Instanz hat und keinen Bastion-Host benötigt.
  • Der Zugriff erfolgt über IAM Users und IAM Policies - keine Verwendung und Verteilung von SSH Key Pairs notwendig.
  • Das Herstellen einer Verbindung, Ausführung von Kommandos und deren Ausgaben können in Cloud Trail, Cloud Watch oder S3 protokolliert werden.

Voraussetzungen

  • Die EC2-Instanz benötigt einen SSM Agent. Dieser ist bereits bei Amazon Linux 2 und Ubuntu Machine Images vorinstalliert. Bei anderen Images kann der Agent problemlos manuell nachinstalliert werden.
  • Der Instanz muss eine Rolle mit der Managed Policy AmazonSSMManagedInstanceCore hinzugefügt werden.
  • Die lokale AWS CLI benötigt die Installation des AWS Session Manager Plugins (siehe Installationsanleitung)

Verbindung via aws ssm

Für die Arbeit im Terminal ist die Nutzung des CLI Tools awsume empfehlenswert. Mit dem Tool lassen sich bequem kurzlebige Session Credentials zur Übernahme von Rollenzugangsdaten in das lokale Environment generieren. Zunächst ist also das “awsumen” einer Rolle notwendig, die mind. die Berechtigungen ssm:StartSession und ssm:TerminateSession besitzt:

awsume {{ profile }}

Im nächsten Schritt ist die Instance ID der gewünschten Instanz in Erfahrung zu bringen. Dies gelingt über die AWS Web Console oder über das Kommando:

aws ec2 describe-instances --query "Reservations[].Instances[].{id:InstanceId,name:Tags[?Key=='Name']|[0].Value}"

Das Kommando listet sowohl Name als auch Instance ID aller EC2-Instanzen des Accounts auf.

Der Verbindungsaufbau gelingt nun via:

aws ssm start-session --target "{{ ec2 instance id }}"

Es ist nun möglich, Kommandos auf der EC2-Instanz auszuführen als sei man via SSH mit ihr verbunden.

Verbindung via aws-connect

aws-connect ist ein Wrapper-Skript um den AWS Session Manager zum Aufbau von Remote-Shell-Verbindungen oder SSH-Tunneln. Damit ist es bspw. möglich die Verbindung über den Name Tag einer Instanz herzustellen, welcher meist einprägsamer ist als die Instance ID.

aws-connect -n {{ ec2 instance name }}

Auch hier sind vorab via awsume entsprechende Credentials zu generieren.

Weitere Informationen zu aws-connect finden sich im GitHub Repository des Projekts.

Verbindung via SSH über aws ssm

Ebenfalls möglich ist ein SSH-Tunnel über aws ssm. Hierzu ist allerdings wieder ein SSH Key notwendig. Zudem ist es so, dass der Session Manager hier nur als Tunnel für die SSH-Verbindung genutzt wird. SSH verschlüsselt die Sitzungsdaten und daher steht in diesem Fall auch keine Protokollierung in AWS zur Verfügung.

Folgende Varianten sind möglich:

  • der eigene SSH Key wird auf der EC2-Instanz unter /home/ec2-user/.ssh/authorized_keys hinterlegt
  • beim Verbindungsaufbau von der lokalen Maschine wird das Key Pair der EC2-Instanz verwendet

Gehen wir vom zweiten Fall aus, so ist in ~/.ssh/config auf der lokalen Maschine die folgende Konfiguration hinzuzufügen:

host i-* mi-*
  ProxyCommand sh -c "aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p'"
  IdentityFile /path/to/key.pem
  User ec2-user

Damit wird für jeden SSH-Verbindungsaufbau gegen einen Host mit dem Pattern i-* oder mi-* aws ssm als Proxy verwendet. Die Angabe von IdentityFile und User bringt etwas Komfort, denn die Verbindung kann nun wie folgt aufgebaut werden:

ssh {{ instance id }}

Ohne die feste Hinterlegung von IdentityFile und User wäre dies:

ssh -i {{ /path/to/key.pem }} ec2-user@{{ instance id }}