Published by

Il y a 2 ans -

Temps de lecture 26 minutes

AWS – Comment sécuriser ses accès aux instances EC2

Introduction

Lorsque l’on utilise des fournisseurs de cloud, il n’est pas si courant que tout puisse être exécuté sur des services managés ou en serverless et on en vient immanquablement à utiliser des machines virtuelles qui vont héberger nos différents composants applicatifs.
Sur AWS, ces machines virtuelles sont nommées EC2 (Elastic Cloud Compute) et ressemblent à n’importe quelle machine virtuelle avec ses interfaces réseau, ses disques, etc.

En tant qu’Ops, il peut m’arriver d’avoir à me connecter sur ces instances, pour différentes raisons :

  • Récupération de logs (idéalement non, mes logs sont collectées sur des systèmes tiers)
  • Routines de maintenance (on a tendance à les automatiser lorsque c’est possible, mais ça n’est pas toujours le cas)
  • Sûrement plein d’autres bonnes raisons.

Aujourd’hui, voyons ensemble les différentes possibilités de connexion sur une instance EC2 et voyons les avantages/inconvénients de chacune.
Chaque solution sera présentée sous forme de code Terraform afin que vous puissiez également tester par vous même.

ℹ L’ensemble du code présenté peut-être retrouvé dans ce dépôt Github

Il est à noter que dans cet article, nous ne nous intéresserons qu’aux possibilités offertes nativement (en dehors d’un client SSH) par AWS.

Nous ne nous pencherons donc pas sur des systèmes externes tels que Hashicorp Vault (qui permet de stocker des clés SSH et les utiliser via OTP), Google Beyond Corp, Teleport ou le tout récemment annoncé Hashicorp Boundary.

Par ailleurs, parmi les avantages et inconvénients de chaque solution, notre fibre sécuritaire interne nous indique également de faire attention à tout ce qui permet d’auditer les éventuelles connexions faites sur les instances.
Car oui, un jour, notre application s’exécutera peut-être en production et peut-être que tout le monde ne devra pas faire ce qu’il veut sur cet environnement. Et peut-être que nous aurons besoin de savoir ce qui a été fait sur une instance à un moment donné.
Peut-être… En tous cas, pensons-y. Merci la fibre.

Allons-y, voyons le sommaire

  • Les méthodes d’accès
    • Connexion avec la clé SSH de création de l’instance
    • Le provisionnement de clés publiques personnelles sur les instances
    • AWS EC2 Instance Connect
      • Les adaptations
      • La connexion
      • L’audit
    • AWS Session Manager
      • Les adaptations
      • La connexion
      • L’audit
      • Les points d’attention
  • Take away

Les méthodes d’accès

Connexion avec la clé SSH de création de l’instance

La première méthode, la plus simple et la plus rapidement rencontrée lorsque l’on travaille avec AWS.
Vous ne le savez peut-être pas, mais pour créer une instance EC2, AWS nous demande de choisir une clé SSH (qu’il faudra avoir téléchargé au préalable sur AWS ou bien qu’AWS va créer à la volée).
Cette clé sera le premier moyen d’accès à votre instance.
C’est ultra simple : vous utilisez votre client SSH habituel et ça marche out-of-the-box.
Pour la création, nous avons besoin de :

  • une clé SSH qui va être uploadée sur AWS
resource "aws_key_pair" "simple_ec2" {
    key_name = "simple_ec2"
    public_key = file(var.key_path)
}
  • un groupe de sécurité avec le port 22 (SSH) ouvert ce qui permet un accès depuis notre IP publique
resource "aws_security_group" "simple_ec2" {
    vpc_id = var.vpc_id
    name = "simple-ec2-ssh "
    description = "security group that allows ssh in"
 
    ingress {
        from_port = 22
        to_port = 22
        protocol = "tcp"
        cidr_blocks = ["${local.ifconfig_co_json.ip}/32"]
    }
    egress {
        from_port = 0
        to_port = 0
        protocol = "-1"
        cidr_blocks = ["0.0.0.0/0"]
    }
    tags = local.tags
}
  • et une instance EC2 dans ce groupe de sécurité
resource "aws_instance" "simple_ec2" {
    ami = data.aws_ami.target_ami.id
    instance_type = "t2.micro"
    subnet_id = data.aws_subnet.selected.id
    vpc_security_group_ids = [aws_security_group.simple_ec2.id]
    associate_public_ip_address = true
    root_block_device {
        volume_type = "standard"
        volume_size = 8
    }
    key_name = aws_key_pair.simple_ec2.key_name
    iam_instance_profile = aws_iam_instance_profile.simple_ec2.name
    tags = local.tags
    volume_tags = local.tags
}

À noter que cette instance devra être accessible depuis Internet (IP publique ou routage Internet) ou il faudra disposer d’une configuration type entreprise qui autorise le lien entre un réseau interne et les réseaux privés AWS.
Une fois la création effectuée avec la commande suivante

terraform apply --var "key_path=/path/to/creation/key.pub" --var "vpc_id=<vpc_id>"

nous pouvons nous y connecter très simplement, en utilisant la clé fournie

ssh -i ~/.ssh/id_rsa ec2-user@$(terraform output instance_public_ip)
ECDSA key fingerprint is SHA256:8kgwO3fpruwejGErpP4gOGNjFh2AOyHi0Y0P7M0FiMM.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '15.188.81.225' (ECDSA) to the list of known hosts.
__| __|_ )
_| ( / Amazon Linux 2 AMI
___|\___|___|
https://aws.amazon.com/amazon-linux-2/
2 package(s) needed for security, out of 13 available
Run "sudo yum update" to apply all updates.
[ec2-user@ip-172-31-2-225 ~]$ echo "`whoami` is connected"
ec2-user is connected

Parfait, ça fonctionne !
En revanche, si cela peut suffire si vous travaillez seul, c’est une autre histoire lorsqu’il s’agit de travailler en équipe.
Imaginez ! Chaque personne de l’équipe doit avoir une copie de cette clé privée.
Mais… une clé privée…partagée… c’est une clé publique, non ? Du coup, ça devient une clé priblique.
Et une clé priblique, ça n’a aucun sens. Non, vraiment.
Voyons donc une autre solution.

Le provisionnement de clés publiques personnelles sur les instances

En provisionnant notre clé publique sur une instance, cela nous donne également la possibilité de nous connecter sur cette instance mais cette fois ci sans avoir à partager quoi que ce soit.
Chacun utilise sa clé personnelle. Et la clé qui a servi à provisionner l’instance peut rester bien au chaud sans être partagée avec quiconque.
Voyons cela en exemple.
Ici, nous allons, pour lancer notre instance, utiliser une clé créée au préalable sur AWS, dont nous n’avons même pas connaissance et qui ne sera partagée avec personne.
Et une seconde clé, la nôtre, que nous allons ajouter dynamiquement sur l’instance lors de sa création (et l’associer à un utilisateur créé au passage).
Ci-dessous, les changements par rapport au cas précédent :

  • Nous allons cette fois ci récupérer le contenu de notre clé publique
data "local_file" "connection_key" {
    filename = var.key_path
}
  • Puis créer un script « user-data » (cf cette page pour le détail) que notre instance va exécuter au démarrage.

Dans celui-ci, nous allons créer un nouvel utilisateur et faire en sorte que l’on puisse se connecter avec cet utilisateur via notre clé SSH personnelle.

data "template_file" "user_data" {
    template = <<EOF
#!/bin/bash -x
 
# First we create a group and a user
groupadd giom
useradd -m -g giom giom
 
# Ensure our public key is present in giom's authorized keys
install -o giom -g giom -m 700 -d /home/giom/.ssh
 
cat >> /home/giom/.ssh/authorized_keys << CFG
${data.local_file.connection_key.content}
CFG
 
# Set the right permissions to ssh files as root write them in user data
chmod 600 /home/giom/.ssh/authorized_keys
chown giom:giom /home/giom/.ssh/authorized_keys
 
EOF
}
  • Et enfin, indiquer d’utiliser ce « user-data » à notre instance et changer la clé utilisée pour la lancer
resource "aws_instance" "provision_key" {
    ami = data.aws_ami.target_ami.id
    instance_type = "t2.micro"
    subnet_id = data.aws_subnet.selected.id
    vpc_security_group_ids = [aws_security_group.provision_key.id]
    associate_public_ip_address = true
    root_block_device {
        volume_type = "standard"
        volume_size = 8
    }
    key_name = var.creation_key_name                                        # Nous utilisons ici la nouvelle clé
    iam_instance_profile = aws_iam_instance_profile.provision_key.name
    tags = local.tags
    volume_tags = local.tags
    user_data = data.template_file.user_data.rendered                       # Nous utilisons également le user-data défini plus haut
}

Et après lancement de l’instance, nous pouvons bien nous y connecter avec notre nouvelle clé.

ssh -i ~/.ssh/id_rsa giom@$(terraform output instance_public_ip)
Last login: Wed Oct 21 22:45:33 2020 from lfbn-idf3-1-1020-206.w90-3.abo.wanadoo.fr
 
__| __|_ )
_| ( / Amazon Linux 2 AMI
___|\___|___|
 
https://aws.amazon.com/amazon-linux-2/
2 package(s) needed for security, out of 13 available
Run "sudo yum update" to apply all updates.
[giom@ip-172-31-10-54 ~]$ echo "`whoami` is connected"
giom is connected

Ici encore, la solution est plutôt simple, mais posons nous la question dudit approvisionnement.
Si votre infrastructure est gérée via du code tel qu’avec CloudFormation ou Terraform :

On peut imaginer que celui-ci sera fait dynamiquement, comme montré plus haut.
C’est une idée. Mais cela va se compliquer lorsqu’une nouvelle personne arrive dans votre équipe :

    • il faudra alors choisir entre ne pas permettre à cette personne d’accéder à l’instance ou a minima compléter le user-data de l’instance et la redémarrer (si ce n’est la recréer et donc en coupant potentiellement le service qu’elle rend).

Si votre infrastructure n’est pas gérée via du code :

Vous devriez sérieusement y songer, pour commencer.

Dans les deux cas, nous pouvons également penser à des solution telles qu’Ansible qui pourraient permettre d’ajouter la nouvelle clé sans avoir à recréer l’instance.
Certes. Mais il faudra alors prévoir que le playbook soit rejoué à chaque fois que l’instance est recrée, sous peine de ne plus pouvoir y accéder.
Et entre nous, cela commence à sérieusement compliquer le déploiement d’une instance, sans parler du fait que l’on jette à la poubelle le principe d’immuabilité.
Voyons d’autres solutions, AWS doit bien proposer des solutions natives…

AWS EC2 Instance Connect

EC2 Instance Connect est un mécanisme offert par AWS qui permet de répondre à la contrainte majeure de la solution précédente : provisionner des clés SSH.
Ici, il devient possible d’envoyer « temporairement » sa clé publique sur une instance (avec une durée limitée à 60 secondes) afin de pouvoir se connecter sur l’instance.
AWS va donc se charger pour nous, à notre demande, de provisionner notre clé publique dans les metadata de l’instance et ensuite d’invalider cette clé publique en la supprimant.
Ainsi, plus de stockage/provisionnement à gérer.
Afin de permettre cela, il nous faut faire quelques modifications

Les adaptations

  • Créer un utilisateur IAM et lui donner les droits d’utiliser EC2 Instance Connect
# ------------------------------------------------------------------------------
# Create an user for our tests
# ------------------------------------------------------------------------------
resource "aws_iam_user" "instance_connect" {
    name = local.name
    tags = merge(map("Name", "instance_connect"), var.tags)
}
 
resource "aws_iam_access_key" "instance_connect" {
    user = aws_iam_user.instance_connect.name
}
 
# ------------------------------------------------------------------------------
# Configure policy to allow users to connect to instance
# ------------------------------------------------------------------------------
data "aws_iam_policy_document" "allow_instance_connect" {
    statement {
        sid = "allowResourceAccessByName"
        effect = "Allow"
        actions = [
            "ec2-instance-connect:SendSSHPublicKey",
        ]
        condition {
            test = "StringEquals"
            values = ["ec2-user"]
            variable = "ec2:osuser"
        }
        resources = [
            "arn:aws:ec2:${data.aws_region.current.id}:${data.aws_caller_identity.current.account_id}:instance/${aws_instance.instance_connect.id}"
        ]
    }
// This statement will give the same access as the previous one, it is just here to show how tags can be handled
    statement {
        sid = "allowResourceAccessByTags"
        effect = "Allow"
        actions = [
            "ec2-instance-connect:SendSSHPublicKey",
        ]
        condition {
            test = "StringEquals"
            values = [local.name]
            variable = "aws:ResourceTag/Name"
        }
        resources = [
            "arn:aws:ec2:${data.aws_region.current.id}:${data.aws_caller_identity.current.account_id}:instance/*"
        ]
    }
 
    statement {
        sid = "AllowDescribeEC2Instances"
        effect = "Allow"
        actions = [
            "ec2:DescribeInstances"
        ]
        resources = ["*"]
    }
}
 
resource "aws_iam_policy" "allow_instance_connect" {
    name = local.name
    path = "/test/"
    description = "Allows use of EC2 instance connect"
    policy = data.aws_iam_policy_document.allow_instance_connect.json
}
 
resource "aws_iam_policy_attachment" "allow_instance_connect" {
    name = local.name
    users = [aws_iam_user.instance_connect.id]
    policy_arn = aws_iam_policy.allow_instance_connect.arn
}

Nous avons également supprimé le user-data de l’instance, car nous allons nous connecter directement sur un utilisateur existant (celui par défaut dans les instances Amazon Linux : ec2-user).
Il reste bien entendu possible de créer par avance des utilisateurs applicatifs avec des droits particuliers et qui seront accédés par les membres de l’équipe.
Également, n’ayant plus besoin de provisionner par avance une clé, nous avons supprimé tout ce qui concerne une éventuelle clé personnelle. Seule reste la clé de création de l’instance.
Une fois les ressources créées, pour nous connecter sur notre instance, la mécanique est un peu plus longue.
Il faut au préalable récupérer les informations de notre instance, y envoyer notre clé publique et ensuite nous connecter.

La connexion

Ci-dessous, le résultat d’un
script
(disponible dans le dépôt également) qui exécute les commandes nécessaires à la connexion sur l’instance

./test_access.sh instance-connect
 
Lets use "instance-connect" instance name
---
 
First we need to gather informations about the instance we want to connect (instance connect use only instance id...)
aws ec2 describe-instances --filters Name=tag:Name,Values=instance-connect Name=instance-state-name,Values=running --query "Reservations[*].Instances[*].[PublicIpAddress,InstanceId,Placement.AvailabilityZone]" --output text
 
---
 
It allows to get instance id, ip and availability zone
instance id : i-08555c2b7e4757114
instance ip: 35.180.134.22
Availability zone : eu-west-3a
 
---
 
Then we use aws-cli to send our public key to the instance metadata. This allow us to connect to the instance within 60s.
aws ec2-instance-connect send-ssh-public-key --availability-zone eu-west-3a --instance-id i-08555c2b7e4757114 --instance-os-user ec2-user --ssh-public-key file:///home/giom/.ssh/id_rsa.pub
 
---
 
{
"RequestId": "7d3f1ee1-dbc8-4d2c-8215-bea616c575e4",
"Success": true
}
 
Now we can connect to the instance with ssh
Connecting to instance...
ssh -i /home/giom/.ssh/id_rsa.pub ec2-user@35.180.134.22
 
---
 
The authenticity of host '35.180.134.22 (35.180.134.22)' can't be established.
ECDSA key fingerprint is SHA256:rTT3Epr3av0/842v3HCFpCk2Hc/jAkIizIT0QQNYGLw.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '35.180.134.22' (ECDSA) to the list of known hosts.
 
__| __|_ )
_| ( / Amazon Linux 2 AMI
___|\___|___|
 
https://aws.amazon.com/amazon-linux-2/
2 package(s) needed for security, out of 13 available
Run "sudo yum update" to apply all updates.
[ec2-user@ip-172-31-7-240 ~]$ echo "`whoami` is connected"
ec2-user is connected
[ec2-user@ip-172-31-7-240 ~]$

À noter qu’il existe un wrapper pour ces opérations (Instance Connect Cli) ou qu’il est encore possible de se connecter via la console AWS mais ces cas ne sont pas couverts ici.
Nous avons maintenant une solution un peu plus sécurisée :

  • Plus de provisionnement long terme des clés SSH
  • Authentification basée sur AWS IAM, ce qui nous permet quand même une souplesse non négligeable sur l’ajout/suppression d’utilisateurs pouvant se connecter aux instances.

En revanche, je ne sais pas si vous vous en souvenez, nous avions promis à notre sympathique fibre sécuritaire de penser à elle lors de cette étude.
Pensons-y justement ! EC2 Instance Connect ajoute une fonctionnalité intéressante : les traces de connexion.

L’audit

Les traces disponibles sont stockées dans CloudTrail Events, et nous pouvons les récupérer assez simplement.

aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=SendSSHPublicKey --max-items=1

Et nous obtenons un JSON contenant la date du dernier envoi de clé SSH sur une instance. Nous pouvons donc obtenir les informations suivantes :

  • L’utilisateur qui a initié l’envoi de la clé.
  • La clé publique envoyée.
  • La ressource de destination.
{
    "Events": [
        {
            "EventId": "7c36a236-0f7e-4686-a8d2-f4eac0f0d9af",
            "EventName": "SendSSHPublicKey",
            "ReadOnly": "false",
            "AccessKeyId": "AKIAYJXMPMO3MLKAZ27A",
            "EventTime": "2020-09-30T14:19:08+02:00",
            "EventSource": "ec2-instance-connect.amazonaws.com",
            "Username": "giom",
            "Resources": [
                {
                    "ResourceType": "AWS::EC2::Instance",
                    "ResourceName": "i-03dab34fbcf4a1e6c"
                }
            ],
            "CloudTrailEvent": "{\"eventVersion\":\"1.05\",\"userIdentity\":{\"type\":\"IAMUser\",\"principalId\":\"AIDAYJXMPMO3AP67IJNV2\",\"arn\":\"arn:aws:iam::570652844982:user/glhermenier\",\"accountId\":\"570652844982\",\"accessKeyId\":\"AKIAYJXMPMO3MLKAZ27Q\",\"userName\":\"glhermenier\",\"sessionContext\":{\"sessionIssuer\":{},\"webIdFederationData\":{},\"attributes    \":{\"mfaAuthenticated\":\"false\",\"creationDate\":\"2020-09-30T12:19:07Z\"}}},\"eventTime\":\"2020-09-30T12:19:08Z\",\"eventSource\":\"ec2-instance-connect.amazonaws.com\",\"eventName\":\"SendSSHPublicKey\",\"awsRegion\":\"eu-west-3\",\"sourceIPAddress\":\"165.225.76.94\",\"userAgent\":\"aws-cli/2.0.7 Python/3.7.3 Linux/5.4.0-48-generic botocore/2.0.0dev11\",\"requestParameters\":{\"instanceId\":\"i-03dab34fbcf4a1e6c\",\"osUser\":\"ec2-user\",\"SSHKey\":{\"publicKey\":\"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCeccFQ7AGIUlvBPVgjuLMuhMzyoc8ZED+dCFBAurn/9rV90zi6VxVle8vRGJ6EUFl8wcUNgL7CPu1FzQc9msFrYf+Kez3WqKFIFFqNluaHwcJmw4b8HhMqLSVY3a47vRfMJ1UV7aK3pv05gTG10b+qSFI9RIaLPNsSIzTXzPOHNDx367WI+NhEpu8TQyLLRmPd3zrp4uiC52KZGT55q4GUiWr01/AedD6hP5wP26qOe2TvM7x6wCNsewcyNC7ARAYrvX0C56qi1IzZtXdLrkSkeArnHAWz7rXByuqwdNrPI7VYNZ/JUEYQwwHXblCv8bJ+vlXQhkbGH9Bbo9rIDKD9FN1F+6vn2gEmyfjvwk+vLukTtXeV1piJHbOfPSa+RITIaxcv804ZI4510slSuk0Tl/hg7QUvGWHU+Hcpa8/GjZ0GvBCaDDLJf8+EPdzE/iqiYIDd1JQvtTZ4zuHn296r2zZD+mrSqf1dYoQw+L6fC/uVfrbyPJKDS04/eNsmmtgvBKWUzpWl6aCDjuJSylKJeaGWGAFn+Nmo4JtWoSdhjD6vhfMXWDfHHyuUC5AgiLjagu5IE1sHrwFrdtCnc1ojJObPXUxsaZp4h+E46C3j2xsIen2SpJPd0n3gn6/n++yPzMIB/Zxbz8zHpSbYfqnH4GU5LkeaiZDwVMBWiw== glhermenier@xebia.fr\\n\"}},\"responseElements\":null,\"requestID\":\"d5be19f3-b905-4f0d-9c0a-a2df54166b8b\",\"eventID\":\"7c36a236-0f7e-4686-a8d2-f4eac0f0d9af\",\"eventType\":\"AwsApiCall\",\"recipientAccountId\":\"570652844982\"}"
        }
    ],
    "NextToken": "eyJOZXh0VG9rZW4iOiBudWxsLCAiYm90b190cnVuY2F0ZV9hbW91bnQiOiAxfQ=="
}

Notre fibre sécuritaire se sent déjà un peu mieux, mais ce n’est pas encore la panacée.
Nous ne récupérons ici que les traces d’envoi de clé, rien n’indique si une connexion a réellement été ouverte par la suite.
L’auditabilité est donc encore un peu à la peine :

S’il est possible de savoir qui a peut-être initié une session SSH, impossible de savoir ce qui a été fait sur l’instance : un flux SSH est chiffré et il est donc impossible d’en connaître le contenu.

Heureusement, AWS a pensé à ce cas et c’est là qu’entre en jeu notre dernière solution…

AWS Session Manager

AWS Session Manager est un peu différent des autres solutions vues précédemment.
Ici tout est basé sur les API AWS et il n’est plus du tout question de SSH.
Nous pourrons donc dire au revoir à l’ouverture du port 22 (SSH) sur nos groupes de sécurité. Cela devient superflu.
Voyons cela en détails.

Les adaptations

Nous supprimons donc l’accès à notre groupe de sécurité sur le port 22

resource "aws_security_group" "session_manager" {
    vpc_id = var.vpc_id
    name = "session_manager"
    description = "security group that does not allow ssh in"
   
    // Cette partie ingress sera abordée un peu plus loin dans l'article
    ingress {
        description = "Allow HTTPS port from VPC for VPC Endpoints"
        from_port   = 443
        to_port     = 443
        protocol    = "TCP"
        cidr_blocks = [data.aws_vpc.current.cidr_block]
    }
    egress {
        from_port = 0
        to_port = 0
        protocol = "-1"
        cidr_blocks = ["0.0.0.0/0"]
    }
    tags = local.tags
}

Pour être certains d’être bien dans une zone privée, sans accès SSH, nous renonçons également à disposer d’une adresse IP publique

resource "aws_instance" "session_manager" {
    ami = data.aws_ami.target_ami.id
    instance_type = "t2.micro"
    subnet_id = data.aws_subnet.selected.id
    vpc_security_group_ids = [aws_security_group.session_manager.id]
    associate_public_ip_address = false # Nous voulons du privé !
    root_block_device {
        volume_type = "standard"
        volume_size = 8
    }
    key_name = var.creation_key_name
    iam_instance_profile = aws_iam_instance_profile.session_manager.name
    tags = local.tags
    volume_tags = local.tags
}

Nous allons également ajouter quelques droits à notre utilisateur IAM :

  • La possibilité de décrire les instances, pour lister les instances auquel il pourra se connecter
  • Démarrer une session SSM sur une instance (liste des instances pouvant être limitée par tags par exemple)
  • Terminer ses propres sessions SSM
data "aws_iam_policy_document" "allow_session_manager" {
    statement {
        sid = "AllowDescribeEC2Instances"
        effect = "Allow"
        actions = [
            "ssm:DescribeSessions",
            "ssm:GetConnectionStatus",
            "ssm:DescribeInstanceProperties",
            "ec2:DescribeInstances"
        ]
        resources = ["*"]
    }
 
    statement {
        sid = "AllowStartSSMSession"
        effect = "Allow"
        actions = ["ssm:StartSession"]
        resources = [
            "arn:aws:ec2:${data.aws_region.current.id}:${data.aws_caller_identity.current.account_id}:instance/*",
            aws_ssm_document.session_manager.arn
        ]
        // Cette condition force l'utilisateur à se connecter en spécifiant un nom de document SSM (et non utliser celui par défaut).
        // Ceci permet par exemple de forcer la restriction d'accès à certaines commandes.
        condition {
            test = "BoolIfExists"
            values = ["true"]
            variable = "ssm:SessionDocumentAccessCheck"
        }
        // Il est également possible de limiter l'accès à des instances particulières, selon leurs tags.
        condition {
            test = "StringEquals"
            values = [local.name]
            variable = "aws:ResourceTag/Name"
        }
    }
 
    statement {
        sid = "AllowTerminateOwnSessionOnly"
        effect = "Allow"
        actions = ["ssm:TerminateSession"] 
        resources = [
            "arn:aws:ssm:${data.aws_region.current.id}:${data.aws_caller_identity.current.account_id}:session/$${aws:username}-*"
        ]
    }
}

Nous devons également ajouter des permissions à notre instance pour qu’elle puisse utiliser les API SSM

data "aws_iam_policy_document" "session_manager" {
    // Permission minimales pour utiliser Session Manager.
    // La politique managée "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" autorise GetParameter sur tous les paramètres, et nous ne voulons par forcément de droits aussi étendus.
    statement {
        sid    = "AllowSSMMessagesUsage"
        effect = "Allow"
        actions = [
            "ssmmessages:CreateControlChannel",
            "ssmmessages:CreateDataChannel",
            "ssmmessages:OpenControlChannel",
            "ssmmessages:OpenDataChannel"
        ]  
        resources = ["*"]
    }
 
    // Les deux permissions suivantes ne sont pas requises pour utiliser Session Manager.
    // En revanche, elles sont utiles si l'on souhaite utiliser SSM RunCommand (pour mise à jour de l'agent SSM par exemple)
    statement {
        sid    = "AllowSSMUsage"
        effect = "Allow"
        actions = [
            "ssm:ListInstanceAssociations",
            "ssm:ListAssociations",
            "ssm:UpdateInstanceInformation"
        ]
        resources = ["*"]
    }
    statement {
        sid    = "AllowEC2MessagesUsage"
        effect = "Allow"
        actions = [
            "ec2messages:GetMessages",
            "ec2messages:AcknowledgeMessage",
            "ec2messages:DeleteMessage",
            "ec2messages:FailMessage",
            "ec2messages:GetEndpoint",
            "ec2messages:GetMessages",
            "ec2messages:SendReply",
            "ec2:DescribeInstanceStatus"
        ]
        resources = ["*"]
    }
}

Et enfin, créer un document SSM qui nous permettra de logger des choses dans Cloudwatch et S3 (nous y reviendrons plus tard).
Autrement, le document par défaut (SSM-SessionManagerRunShell) serait suffisant.

resource "aws_ssm_document" "session_manager" {
    name = "SSM-SessionManagerRunShell-${local.name}"
    document_type = "Session"
    document_format = "JSON"
    tags = local.tags
 
    content = jsonencode({
        schemaVersion = "1.0"
        description = "Document to hold settings for Session Manager"
        sessionType = "Standard_Stream"
        inputs = {
            // Ces 3 propriétés permettent le logging dans S3. Cf plus bas
            s3BucketName = aws_s3_bucket.session_manager.bucket
            s3KeyPrefix = local.name
            s3EncryptionEnabled = true
            // Ces 2 propriétés permettent le logging dans cloudwatch. Cf plus bas
            cloudWatchLogGroupName = aws_cloudwatch_log_group.session_manager.name
            cloudWatchEncryptionEnabled = true
        }
    })
}

La connexion

Une ouverture de session va passer par le service AWS Session Manager qui va :

  • authentifier la demande d’accès
  • vérifier si l’utilisateur faisant l’appel dispose bien des autorisations nécessaires pour accéder à l’instance par ce moyen
  • vérifier si l’utilisateur doit se voir appliquer des restrictions particulières sur cette session et les appliquer (par exemple des restrictions sur les commandes autorisées)
  • et enfin, joindre l’agent SSM déployé sur l’instance (il s’agit d’un prérequis, vous l’aurez deviné. Il est déployé par défaut sur Amazon Linux, sinon vous référez à cette documentation) qui va initier une communication bidirectionnelle avec l’utilisateur.

Dans la réalité, cela nous donne

./test_access.sh $(terraform output instance_name) $(terraform output ssm_document_name)
 
Retrieve instance id from aws-cli using tag Name
aws ec2 describe-instances --filter "Name=tag:Name,Values=session-manager" "Name=instance-state-name,Values=running" --query "Reservations[*].Instances[*].{Instance:InstanceId}" --output=text | head -n1
 
---
 
Then launch a new session
aws ssm start-session --target=i-09769e0bb43b69a3d --document-name SSM-SessionManagerRunShell-session-manager
and run some commands
 
---
 
Starting session with SessionId: session-manager-07f3cd5f6ff3628ab
sh-4.2$ echo "`whoami` is connected"
ssm-user is connected
sh-4.2$ pwd
/usr/bin
sh-4.2$ exit
Exiting session with sessionId: session-manager-07f3cd5f6ff3628ab.

Il est également possible de se connecter via la console AWS.
Et si en plus nous annonçons à notre fibre sécuritaire que ladite communication bi-directionnelle est :

  • chiffrée via TLS1.2
  • authentifiée via les mécanimes de signature AWS (sigV4 à l’heure de rédaction de cet article)
  • possiblement surchiffrée avec AWS KMS

alors elle devrait être déjà bien satisfaite.
Mais ce qui lui fera encore plus plaisir, c’est que l’auditabilité soit également au rendez-vous !

L’audit

Toute session ouverte sur l’instance via une connexion Session Manager occasionnera (si c’est configuré) l’enregistrement de ladite session (sur Cloudwatch ou un bucket S3, qui eux-aussi pourront être chiffrés).
Et la connexion via un tunnel (comme pour SSH) est également de la partie. On ne perd donc pas grand chose !
Voici un exemple de récupération de contenu d’une session

./check_access.sh
 
Retrieve latest sessions from ssm sessions history
aws ssm describe-sessions --state History --filter "key=Owner,value=arn:aws:iam::570652844982:user/session-manager"
 
---
{
    "Sessions": [
        {
            "SessionId": "session-manager-07f3cd5f6ff3628ab",
            "Target": "i-09769e0bb43b69a3d",
            "Status": "Terminated",
            "StartDate": "2020-10-22T03:30:39.221000+02:00",
            "EndDate": "2020-10-22T03:31:01.054000+02:00",
            "DocumentName": "SSM-SessionManagerRunShell-session-manager",
            "Owner": "arn:aws:iam::570652844982:user/session-manager",
            "OutputUrl": {
                "S3OutputUrl": "https://eu-west-3.console.aws.amazon.com/s3/object/session-manager-570652844982/session-manager/session-manager-07f3cd5f6ff3628ab.log",
                "CloudWatchOutputUrl": "https://eu-west-3.console.aws.amazon.com/cloudwatch/home#logEventViewer:group=/test/session-manager;stream=session-manager-07f3cd5f6ff3628ab"
            }
        }
    ]
]

Nous récupérons ici tout ce qui concerne la dernière session d’un utilisateur donné.
Le contenu de la session est alors disponible dans S3 ou Cloudwatch, selon ce que vous avez configuré (ou les deux, avec des rétentions différentes !)
Je vous invite à consulter le dépôt sur lequel se base cet article pour voir comment mettre en place le logging vers Cloudwatch ou S3 (ainsi que le chiffrement qui va bien avec).
Voici un exemple de contenu qu’il est possible de récupérer depuis Cloudwatch et qui correspond à la session que nous avons ouverte plus haut.

2020-10-22T03:30:58.045+02:00
Script started on 2020-10-22 01:30:54+0000
[?1034hsh-4.2$
[Ksh-4.2$ echo "``w`h`o`a`m`i`[C is connected"
ssm-user is connected
sh-4.2$ pwd
/usr/bin
sh-4.2$ exit
 
Script done on 2020-10-22 01:30:54+0000

On retrouve donc bien l’ensemble des commandes exécutées lors de la session.
Il est cependant à noter deux choses au sujet de Session Manager.

Les points d’attention

ℹ Connection SSH via Session Manager
1/ Il est en fait possible de se connecter en SSH via Session Manager
Pour ce faire, il est nécessaire d’ajouter la permission suivante à votre utilisateur :

arn:aws:ssm:*:*:document/AWS-StartSSHSession

Puis de configurer votre client SSH comme suit :

host i-* mi-*
    ProxyCommand sh -c "aws ssm start-session --target %h --document-name <YourDocumentName> --parameters 'portNumber=%p'"

En revanche, il faut savoir que toute la partie auditabilité sera perdue. En effet, comme il s’agit d’un flux SSH qui sera ouvert (et donc chiffré), AWS sera dans l’impossibilité d’avoir connaissance du flux, donc de l’enregistrer où que ce soit.

ℹ Mise en place de point de terminaisons VPC
2/ L’accès à une instance EC2 privée nécessite une des options suivantes :

  • Disposer d’une adresse IP publique (et qui soit donc routée sur Internet par défaut)
  • Être dans un sous réseau (public ou privé) disposant d’une route vers Internet (via NAT Gateway par exemple)
  • Être dans une configuration d’entreprise avec une connexion pairée avec le réseau interne de l’entreprise
  • Mettre en place des VPC Endpoints, comme expliqué ici. Un VPC Endpoint permet d’ouvrir des routes entre différents services privés, sans avoir besoin de passer par Internet. Ainsi, tout reste dans le réseau interne AWS. Il s’agit donc d’une option plus sécurisée (mais également non gratuite)

En effet, pour être « managé » par SSM, une instance a besoin de se connecter au service SSM, qui est par défaut exposé sur Internet. Ainsi, une instance privée n’ayant aucun accès Internet ne pourra être jointe.
Auquel cas il est conseillé d’utiliser la dernière solution (VPC Endpoint).
C’est celle-ci qui est utilisée dans l’exemple de code du dépôt

ℹ shell_sh

3/ Vous l’aurez peut-être noté, lorsque vous vous connectez sur une instance EC2 via SSM Session Manager, vous arrivez sur une invite de commande shell classique (et non bash, comme on le rencontre souvent).
La raison à ceci est historique et liée à des bugs rencontrés dans le logging bash.
Dans ce bug remonté sur github, une description plus détaillée est donnée par le support AWS :

The main reason session manager is not using « bash » as default shell is because of logging. When using « bash » shell, session log files that are generated have formatting issues and gibberish characters. Due the logging issue on bash, session manager launches « sh » as a default shell at the moment.

They have also noted that they are currently working on implementing « bash » but they have to research and look for workarounds to improve logging which I believe might take some time.

Tout ceci ne devrait bientôt plus être qu’un lointain souvenir depuis la récente annonce (21/10/2020) du support de la configuration des shells dans AWS SSM.
Un exemple est même donné pour utiliser bash, via l’ajout de

shellProfile = {
    linux = "bash"
}

dans la propriété inputs de la ressource aws_ssm_document (et également disposer a minima de la version 3.0.196.0 de l’agent SSM).

Take away

Nous avons donc vu un panel complet des solutions pour se connecter sur des instances AWS EC2.
À vous de positionner le curseur pour votre propre cas, selon le niveau d’agnosticité/sécurité/auditabilité que vous souhaitez avoir.
Pour conclure, voici un petit rappel des avantages/inconvénients de chaque solution.

Connexion avec la clé SSH de création de l’instance

Avantages Inconvénients
Aucune action requise Besoin de partager la clé privée (Clé privée non privée, tout ça…)
Fonctionne « out of the box » Auditabilité limitée aux logs de connexion SSH. Impossible de savoir qui a fait quoi sur le serveur (il y a toujours les mécanismes d’historique, mais ils sont manipulables)
Nécessite de créer une instance accessible depuis Internet (ou d’avoir une configuration d’entreprise qui permet l’accès aux sous réseaux privés AWS)

Le provisionnement de clés publiques personnelles sur les instances

Avantages Inconvénients
Ajoute du contrôle sur qui peut se connecter à une instance Besoin d’une solution complémentaire pour ajouter/supprimer des clés publiques d’instances déjà déployées
Aucune auditabilité. Impossible d’identifier simplement qui s’est connecté sur un serveur et ce qu’il y a fait
Nécessite de créer une instance accessible depuis Internet (ou d’avoir une configuration d’entreprise qui permet l’accès aux sous réseaux privés AWS)

AWS EC2 Instance Connect

Avantages Inconvénients
Ajoute du contrôle sur qui peut se connecter à une instance Traçabilité limitée à l’envoi de clés sur une instance. Impossible de savoir si une connection a été réellement initiée
Simplifie grandement le processus de déploiement des clés publiques Pas d’auditabilité sur les actions effectuée sur l’instance
Plus besoin de déploiement de clés à long terme Ajout d’une surcouche à la connexion native SSH (ou utilisation de Instance Connect Cli)
Connection autorisée sur un laps de temps réduit (60s par défaut)
Possibilité d’affiner les droits sur des instances : en se servant de tags, on peut autoriser un groupe d’utilisateurs sur certaines instances seulement
Bribes de traçabilité des connections via CloudTrail events

AWS Session Manager

Avantages Inconvénients
Solution totalement intégrée avec les autres services AWS (permissions IAM, restrictions sur des tags… ) Plus de SSH (mais est-ce réellement un inconvénient ?)
Permet de désactiver l’accès en SSH à l’ensemble des instances (suppression du port 22 dans les groupes de sécurité) Nécessite des mécanismes complémentaires en cas d’accès à une zone privée (VPC Endpoints)
Traçabilité des connections via l’historique des sessions SSM
Auditabilité des actions effectuées possible dans S3 ou Cloudwatch

Published by

Commentaire

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Sapient, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.