la PKI Vault sur le grill, nous regardons ici comment Vault peut s'intégrer dans l'environnement AWS.
Notre cas d'usage d'origine était basé sur Puppet et sur le déploiement des certificats par ses agents. J'ai choisi ici de refaire la démo de zéro en utilisant Terraform et Ansible pour des raisons pratiques. À cette différence près, il s'agit de la suite directe du premier article et je vous invite à le lire pour suivre celui-ci.
L'intégration de Vault aux services d'AWS peut se faire à trois endroits :
N'ayant pas eu l'usage du secret backend AWS, je n'aborde que les deux autres intégrations dans les lignes qui suivent.
La consommation des services AWS suppose généralement l'utilisation d'un couple de clés (access_key et secret_key), qu'il est possible de renseigner en paramètres dans la configuration de Vault. Dans ce cas, ces secrets resteraient sur le disque et ce n'est pas la manière la plus sécurisée de faire. D’autre part, le renouvellement de ces clés serait à notre charge et à effectuer régulièrement.
Vault permet de s'appuyer sur le système d'Instance Profile fourni par AWS, à savoir utiliser des credentials temporaires et générés pour une instance EC2 donnée. Ces credentials sont disponibles dans les métadonnées de l'instance. AWS gère la rotation de ces secrets, ainsi que leur expiration si les droits viennent à être mis à jour ou révoqués.
Délégation de droits à une instance EC2
Les credentials sont générés spécifiquement pour l'instance EC2 courante, et sont limités aux droits que vous aurez donné à cette instance. AWS ne génèrera d'ailleurs les paires de clés que si un Instance Profile est attribué à l'instance EC2 en question.
Sans rentrer réellement dans les détails du modèle IAM d'Amazon, voici un exemple des ressources AWS et de leurs associations pour autoriser l'accès à un bucket S3 (écrit ici pour Terraform) :
Instance EC2, associée à son Instance Profile.
Le role, autorisant l'accès au service STS et donc à la génération de credentials pour les instances EC2.
Role policy, ajoutant les droits d'accès RW sur le bucket S3 'sbo-vault'.
Une fois ces ressources AWS allouées, nous sommes capables d'instancier une VM EC2 et de lui donner les droits en écriture sur le bucket S3 de notre choix (à créer par ailleurs).
Si vous voulez approfondir les arcanes de l'IAM AWS, voici quelques pointeurs dans la documentation :
Une fois la partie IAM préparée, la configuration de Vault est simplissime et se limite à définir la région AWS et le nom du bucket S3 :
backend "s3" {
bucket = "sbo-vault"
region = "eu-west-1"
}
listener "tcp" {
address = "127.0.0.1:8200"
tls_disable = 1
}
Au démarrage Vault accède directement au bucket et y écrit ses données. Après l'initialisation de Vault le contenu du bucket ressemble à ceci :
Si vous souhaitez multiplier les instances de Vault, un bucket S3 est nécessaire pour chaque instance déployée. La concurrence en écriture n'est en effet pas gérée et vous risqueriez de corrompre des secrets.
Par ailleurs, S3 n'est pas éligible comme backend de HA : Vault s'appuie sur son backend de stockage pour élire le nœud maître lorsque déployé en cluster, et S3 ne permet pas cette élection.
Pour DynamoDB, le fonctionnement est identique à S3 : une fois les bons droits accordés à l'instance, Vault est capable de récupérer les credentials AWS et de se connecter à DynamoDB. Il y a quelques paramètres supplémentaires comparativement à S3 (disponibles ici [EN]), notamment liés au fonctionnement de la Haute Disponibilité.
Bien que référencé comme backend éligible à la HA, DynamoDB souffre d'une limitation nécessitant une intervention manuelle pour gérer la reprise sur erreur.
Si la haute disponibilité n'est pas critique pour votre cas d'usage (typiquement si Vault est utilisé comme PKI et ne fourni qu'un certificat de temps en temps), l'utilisation d'une instance seule "backée_"_ S3 est largement suffisante. Par ailleurs, l’utilisation d’un Autoscaling Group avec un min/max à 1 permet de maintenir cette unique instance up and running en permanence.
En revanche, si la haute disponibilité est essentielle, je vous invite à regarder Consul, etcd voir ZooKeeper comme backends de HA, tout en gardant S3 comme backend principal. En résumé, pour ce qui est de la HA :
Maintenant que nos secrets sont stockés dans S3, intéressons-nous à la façon de requêter ces secrets depuis l'environnement AWS. Comme décrit dans l'article précédent, Vault possède plusieurs secret backends. Et sur chacun de ces backends les ressources exposées peuvent être associées à un ensemble de politiques de sécurité (policies).
Ces policies sont associées à des utilisateurs ou à des groupes d'utilisateurs, et permettent de définir leurs droits sur les ressources.
Petite subtilité : dans notre exemple du backend PKI, les roles représentent les ressources exposées par le backend PKI. Il ne faut donc pas confondre avec la notion commune du rôle au sens RBAC.
Afin de protéger l'émission de certificats, revenons sur ce que nous avions déjà fait :
$ vault list interca/roles/
aws-dot-octo
Voici le role que nous avions créé la dernière fois (Cf la PKI Vault sur le grill). Et pour mémoire, il suffit de requêter ce role pour obtenir un nouveau certificat :
$ vault write interca/issue/aws-dot-octo common_name=vault.aws.octo
Key Value --- ----- lease_id interca/issue/aws-dot-octo/0d7a8b4f-7f36-c6e1-0ebb-f2f9b0dca194 lease_duration 71h59m59s lease_renewable false ca_chain [...] certificate [...] issuing_ca [...] private_key [...] private_key_type rsa serial_number 6c:98:f6:9b:80:bc:89:32:02:e2:47:34:dc:a9:8c:30:94:00:be:9f
Nous pouvons générer ce certificat car nous utilisons actuellement le token généré à l'initialisation de l'instance Vault. Autrement dit, nous sommes par défaut authentifiés comme root auprès de Vault et avons les droits correspondants.
$ env
[...] VAULT_ADDR=http://127.0.0.1:8200 VAULT_TOKEN=281963cf-88ae-1027-0dcb-ae0a1f0cf3e9
Dans un environnement de production, la création d'utilisateurs aux droits limités et dédiés à certains usages est indispensable, et c'est ce que la création d'une policy va nous permettre de faire.
Par défaut, un client authentifié (autre que root) n'a aucun droit. Il suffit donc de lui associer la policy suivante pour ne l'autoriser qu'à écrire sur notre role :
path "interca/issue/aws-dot-octo" {
policy = "write"
}
Si vous êtes familier des règles de contrôle d'accès sur les ressources web, vous êtes en terrain connu : on associe un chemin de l'URL avec un droit d'accès. Et il n'y a pas de limite dans le nombre de règles dans une même policy. Vous trouverez dans la doc [EN] les différents verbes et capabilities applicables sur les ressources.
Il faut ensuite passer par un fichier pour enregistrer cette policy , que je nomme policy_aws-dot-octo
:
$ vault policy-write policy_aws-dot-octo /tmp/pki-policy.hcl
Policy 'policy_aws-dot-octo' written.
Et voici comment la tester:
$ vault token-create -policy=policy_aws-dot-octo
Key Value --- ----- token 06c97184-3b53-8ddd-f927-7f1a358e8269 token_accessor cc5063ff-2dde-2089-48b7-0f2a753ad985 token_duration 768h0m0s token_renewable true token_policies [default policy_aws-dot-octo]
$ export VAULT_TOKEN=06c97184-3b53-8ddd-f927-7f1a358e8269
$ vault write interca/issue/aws-dot-octo common_name=test-policy.aws.octo
Key Value --- ----- lease_id interca/issue/aws-dot-octo/86eb0d30-8d59-27f1-4d04-e1a11a1ed40c lease_duration 71h59m59s lease_renewable false ca_chain [...] certificate [...] issuing_ca [...] private_key [...] private_key_type rsa serial_number 7e:9e:eb:2f:a7:ff:c7:4b:62:e6:f5:30:49:43:a7:21:2d:46:aa:c3
$ vault mounts
Error reading mounts: Error making API request.
URL: GET http://127.0.0.1:8200/v1/sys/mounts Code: 403. Errors:
Ce qui s'est passé :
policy_aws-dot-octo
.403 : Forbidden
.Tadaaa !
Nous venons d'utiliser les privilèges accordés à root pour générer le token d'un utilisateur. Transposé dans le monde réel cela revient à passer par un administrateur pour demander la création de credentials de manière unitaire, et c'est éloigné de ce que l'on veut faire : automatisation / répudiation & expiration des credentials / passage à l'échelle etc.
Vault nous propose des backends d'authentification pour nous appuyer sur des solutions existantes. Au delà de la gestion de l'authentification par tokens (intégrée dans le cœur de Vault), vous pouvez utiliser les traditionnels login + mot de passe, un annuaire LDAP, les certificats x509 ou même GitHub. Dans le cas de GitHub, il est par exemple possible d'accorder des droits d'accès en fonction de l'organisation GitHub à laquelle est rattaché l'utilisateur.
Afin de revenir à un fonctionnement unique, tous les backends d'authentification permettent in fine d'obtenir un token Vault. Et comme dans notre exemple, le token obtenu est scopé sur une policy à sa création. La fin de la séquence est donc identique quelque soit le moyen d'authentification : requête sur l'API Vault avec le token en paramètre.
Vault nous propose de nous appuyer sur l'infrastructure d'Amazon pour réaliser l'authentification des machines EC2 clientes auprès de Vault.
Toute machine virtuelle du service EC2 peut requêter l'URL http://169.254.169.254/latest/dynamic/instance-identity/document
:
$ curl http://169.254.169.254/latest/dynamic/instance-identity/document
{ "devpayProductCodes" : null, "availabilityZone" : "eu-west-1c", "privateIp" : "192.168.1.134", "version" : "2010-08-31", "region" : "eu-west-1", "instanceId" : "i-045d4683886353b08", "billingProducts" : null, "instanceType" : "t2.small", "accountId" : "218232161888", "architecture" : "x86_64", "kernelId" : null, "ramdiskId" : null, "imageId" : "ami-6f587e1c", "pendingTime" : "2017-03-24T15:13:59Z" }
Elle obtient ainsi quelques informations la concernant, comme son adresse IP, son ID et quelques autres métadonnées. De la même manière, avec l'URL http://169.254.169.254/latest/dynamic/instance-identity/pkcs7
, les instances obtiennent la signature de cette même carte d'identité, au format PKCS7 et fournie par Amazon :
$ curl http://169.254.169.254/latest/dynamic/instance-identity/pkcs7
MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCAJIAEggGwewog ICJkZXZwYXlQcm9kdWN0Q29kZXMiIDogbnVsbCwKICAiYXZhaWxhYmlsaXR5Wm9uZSIgOiAiZXUt d2VzdC0xYyIsCiAgInByaXZhdGVJcCIgOiAiMTkyLjE2OC4xLjEzNCIsCiAgInZlcnNpb24iIDog IjIwMTAtMDgtMzEiLAogICJyZWdpb24iIDogImV1LXdlc3QtMSIsCiAgImluc3RhbmNlSWQiIDog ImktMDQ1ZDQ2ODM4ODYzNTNiMDgiLAogICJiaWxsaW5nUHJvZHVjdHMiIDogbnVsbCwKICAiaW5z dGFuY2VUeXBlIiA6ICJ0Mi5zbWFsbCIsCiAgImFjY291bnRJZCIgOiAiMjE4MjMyMTYxODg4IiwK ICAiYXJjaGl0ZWN0dXJlIiA6ICJ4ODZfNjQiLAogICJrZXJuZWxJZCIgOiBudWxsLAogICJyYW1k aXNrSWQiIDogbnVsbCwKICAiaW1hZ2VJZCIgOiAiYW1pLTZmNTg3ZTFjIiwKICAicGVuZGluZ1Rp bWUiIDogIjIwMTctMDMtMjRUMTU6MTM6NTlaIgp9AAAAAAAAMYIBGDCCARQCAQEwaTBcMQswCQYD VQQGEwJVUzEZMBcGA1UECBMQV2FzaGluZ3RvbiBTdGF0ZTEQMA4GA1UEBxMHU2VhdHRsZTEgMB4G A1UEChMXQW1hem9uIFdlYiBTZXJ2aWNlcyBMTEMCCQCWukjZ5V4aZzAJBgUrDgMCGgUAoF0wGAYJ KoZIhvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMTcwMzI0MTUxNDAwWjAjBgkq hkiG9w0BCQQxFgQU+O2Cc6Uk3aEX7F4DLqAM84G9duAwCQYHKoZIzjgEAwQvMC0CFFgm3Pe+OEVq phaXs78dKkYNIfeyAhUArmf9jgRJLfubT7g/WhWqyx4+kKUAAAAAAAA=
AWS nous donne les moyens de vérifier la signature [EN] avec la clé publique correspondante, et Vault se base sur ces éléments pour authentifier une instance. La séquence :
La principale faiblesse de ce moyen d'authentification est le non renouvellement des fiches d'identité des instances EC2. Elles restent identiques jusqu'à la destruction des instances. Afin de prévenir le vol et le rejeu d'une fiche d'identité pendant la vie de l'instance, Vault ajoute la possibilité de créer un nonce [FR] à la première authentification. Une fois le nonce configuré, l'instance en question ne pourras plus s'authentifier auprès de Vault, sans fournir sa fiche d'identité et son nonce simultanément.
Au delà de la phase d'authentification, Vault permet d'ajouter une étape d'autorisation avant de fournir le token. La création d'un role (/!\ cette fois au sens RBAC) dans le backend d'autorisation aws-ec2
, permet de ségréger les instances EC2 à partir des éléments suivants :
Cette liste s'allonge et se complète régulièrement avec les nouvelles versions de Vault.
D'autres éléments sont configurables au niveau du role, comme le TTL des tokens générés pour ce role, ou la policy à associer au role. Vous trouverez le détails des paramètres dans la documentation de l'API du backend auth-ec2
[EN].
La première étape consiste à étendre les droits AWS accordés à l'instance EC2 qui héberge notre Vault. Sans ça, Vault ne sera pas en mesure de vérifier l'état des instances EC2 du tenant. La policy AWS suivante vient donc compléter celle déjà créée pour permettre l'accès au bucket S3 :
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "ec2:DescribeInstances",
"Resource": "*"
}]
}
À cette étape, si nous essayons de nous authentifier à partir de l'instance cliente cela ne fonctionnera pas. Vault impose de créer au moins un role afin de spécifier la politique d'émission du token. Nous allons donc créer un role sur aws-ec2
pour nos instances clientes, et les autoriser selon l'AMI utilisée :
$ vault write auth/aws-ec2/role/vault-client bound_ami_id=ami-6f587e1c policies=policy_aws-dot-octo
Success! Data written to: auth/aws-ec2/role/vault-client
Maintenant, testons depuis l'instance cliente. D'abord la récupération de la fiche d'identité au format PKCS7 (en enlevant les sauts de ligne) :
$ curl -s http://169.254.169.254/latest/dynamic/instance-identity/pkcs7 | tr -d
MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCAJIAEggGvewogICJkZXZwYXlQcm9kdWN0Q29kZXMiIDogbnVsbCwKICAiYXZhaWxhYmlsaXR5Wm9uZSIgOiAiZXUtd2VzdC0xYyIsCiAgInByaXZhdGVJcCIgOiAiMTkyLjE2OC4xLjg1IiwKICAidmVyc2lvbiIgOiAiMjAxMC0wOC0zMSIsCiAgInJlZ2lvbiIgOiAiZXUtd2VzdC0xIiwKICAiaW5zdGFuY2VJZCIgOiAiaS0wZTBjOWZlYmVjY2YzYTIwYiIsCiAgImJpbGxpbmdQcm9kdWN0cyIgOiBudWxsLAogICJpbnN0YW5jZVR5cGUiIDogInQyLnNtYWxsIiwKICAiYWNjb3VudElkIiA6ICIyMTgyMzIxNjE4ODgiLAogICJhcmNoaXRlY3R1cmUiIDogIng4Nl82NCIsCiAgImtlcm5lbElkIiA6IG51bGwsCiAgInJhbWRpc2tJZCIgOiBudWxsLAogICJpbWFnZUlkIiA6ICJhbWktNmY1ODdlMWMiLAogICJwZW5kaW5nVGltZSIgOiAiMjAxNy0wMy0yNFQxNToxMzo1OVoiCn0AAAAAAAAxggEXMIIBEwIBATBpMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQwIJAJa6SNnlXhpnMAkGBSsOAwIaBQCgXTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xNzAzMjQxNTE0MDBaMCMGCSqGSIb3DQEJBDEWBBRhci4q0Js+77sS6PD2hE62+4kPOjAJBgcqhkjOOAQDBC4wLAIUFWPouht+AvoDDZNESGfiTyj5kfQCFFvNVOqgjJU+REbegK/sCYTUNv9RAAAAAAAA
Puis, toujours depuis l'instance cliente, authentification auprès de l'API Vault :
$ curl -X POST "https://vault.aws.octo/v1/auth/aws-ec2/login" -d '{ "role":"vault-client", "pkcs7":"MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIICJkZXZwYXlQcm9kdWN0Q29kZXMiIDogbnVsbCwKICAiYXZhaWxhYmlsaXR5Wm9uZSIgOiAiZXUtd2VzdC0xYyIsCiAgInByaXZhdGVJcCIgOiAiMTkyLjE2OC4xLjg1IiwKICAidmVyc2lvbiIgOiAiMjAxMC0wOC0zMSIsCiAgInJlZ2lvbiIgOiAiZXUtd2VzdC0xIiwKICAiaW5zdGFuY2VJZCIgOiAiaS0wZTBjOWZlYmVjY2YzYTIwYiIsCiAgImJpbGxpbmdQcm9kdWN0cyIgOiBudWxsLAogICJpbnN0YW5jZVR5cGUiIDogInQyLnNtYWxsIiwKICAiYWNjb3VudElkIiA6ICIyMTgyMzIxNjE4ODgiLAogICJhcmNoaXRlY3R1cmUiIDogIng4Nl82NCIsCiAgImtlcm5lbElkIiA6IG51bGwsCiAgInJhbWRpc2tJZCIgOiBudWxsLAogICJpbWFnZUlkIiA6ICJhbWktNmY1ODdlMWMiLAogICJwZW5kaW5nVGltZSIgOiAiMjAxNy0wMy0yNFQxNToxMzo1OVoiCn0AAAAAAAAxggEXMIIBEwIBATBpMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQwIJAJa6SNnlXhpnMAkGBSsOAwIaBQCgXTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xNzAzMjQxNTE0MDBaMCMGCSqGSIb3DQEJBDEWBBRhci4q0Js+77sS6PD2hE62+4kPOjAJBgcqhkjOOAQDBC4wLAIUFWPouht+AvoDDZNESGfiTyj5kfQCFFvNVOqgjJU+REbegK/sCYTUNv9RAAAAAAAA", "nonce":"vault-client-nonce"}'
{ "request_id":"60567c2f-8e27-89e8-b4b9-90acfd4360d7", "lease_id":"", "renewable":false, "lease_duration":0, "data":null, "wrap_info":null, "warnings":null, "auth":{ "client_token":"2c23cd00-05b0-4227-5197-904918db742b", "accessor":"a683bd5e-0c0b-0604-fbd9-f944f5366d4c", "policies":["default","policy_aws-dot-octo"], "metadata":{ "account_id":"218232161888", "ami_id":"ami-6f587e1c", "instance_id":"i-0e0c9febeccf3a20b", "nonce":"vault-client-nonce", "region":"eu-west-1", "role":"vault-client", "role_tag_max_ttl":"0s" }, "lease_duration":2764800, "renewable":true } }
En version plus condensée et sans faire de copier coller :
$ curl -X POST "https://vault.aws.octo/v1/auth/aws-ec2/login" -d "{ \"role\":\"vault-client\", \"pkcs7\":\"$(curl -s http://169.254.169.254/latest/dynamic/instance-identity/pkcs7 | tr -d '\n')\", \"nonce\":\"vault-client-nonce\"}"
{ "request_id":"db499fab-f0c8-0a5a-e964-eca68d16b30b", "lease_id":"", "renewable":false, "lease_duration":0, "data":null, "wrap_info":null, "warnings":null, "auth":{ "client_token":"8846033c-3f05-3e7a-52ce-1de876efd2ff", "accessor":"3dfa5e3f-fe2c-dfc1-cd04-8554e894368b", "policies":["default","policy_aws-dot-octo"], "metadata":{ "account_id":"218232161888", "ami_id":"ami-6f587e1c", "instance_id":"i-0e0c9febeccf3a20b", "nonce":"vault-client-nonce", "region":"eu-west-1", "role":"vault-client", "role_tag_max_ttl":"0s" }, "lease_duration":2764800, "renewable":true } }
Dernière étape : générer un certificat en appelant l'API à l'aide du token obtenu :
$ curl -X POST "https://vault.aws.octo/v1/interca/issue/aws-dot-octo" -H "X-Vault-Token:8846033c-3f05-3e7a-52ce-1de876efd2ff" -d '{"common_name":"test.aws.octo","format":"pem"}'
{"request_id":"5df91a88-f1fd-c291-8b0f-06c78b8f3121", "lease_id":"interca/issue/aws-dot-octo/5ec630f0-b7ea-d606-488d-0058dc975ffc", "renewable":false, "lease_duration":259199, "data":{ "ca_chain": ["-----BEGIN CERTIFICATE-----\n [...]\n -----END CERTIFICATE-----"], "certificate": "-----BEGIN CERTIFICATE-----\n [...]\n -----END CERTIFICATE-----", "issuing_ca": "-----BEGIN CERTIFICATE-----\n [...]\n -----END CERTIFICATE-----", "private_key": "-----BEGIN RSA PRIVATE KEY-----\n [...]\n -----END RSA PRIVATE KEY-----", "private_key_type":"rsa", "serial_number":"6a:5a:04:81:97:e8:6e:b3:80:e8:bb:f0:2b:e0:87:24:2a:7b:2e:fd"}, "wrap_info":null, "warnings":null,"auth":null}
Le duo Ansible / Terraform fonctionne bien, et permet de prototyper plus rapidement qu'avec Puppet / Terraform. Dénominateur commun, Terraform reste dans tous les cas incontournable. Nous le préconisons d’ailleurs quasi-systématiquement sur nos missions AWS, malgré le langage HCL parfois bancal ou une gestion des state un peu fragile notamment.
En termes de maîtrise, difficile de se sentir expert Vault après ce POC tant les concepts changent en fonction des backends utilisés. Le produit évolue rapidement : aujourd'hui testé en 0.6.5, une part des fonctionnalités présentées ont déjà été complétées en 0.7.0 puis 0.7.1. (Un coup d'œil sur le changelog et vous verrez que le backend auth-ec2 étend déjà l'authentification aux personnes, aux lambdas, aux instances ECS etc. et non plus aux simples instances EC2).
Quant à l'intégration à AWS, il semble indispensable d’en comprendre les concepts de leur modèle de droits et d’identités (IAM) avant de se lancer en production. Cela rend la courbe d'apprentissage certes plus pentue, mais devrait éviter la fuite de secrets de votre coffre fort.
En bref, Vault répond à mes attentes en termes d'automatisation, d'intégration, de scalabilité et de fonctionnalités. Je m’attends à le re-croiser régulièrement dans de futures missions ...