Dans le 1er article, nous avons balayé l’ensemble des aspects "sécurité", couverts par le module. Passons à la pratique.
Pour des raisons de simplicité, le code construit, dans un seul composant terraform, l’ensemble des ressources nécessaires pour la démonstration. De même, la base de données et l’instance Elasticsearch seront exposées publiquement mais avec un accès restreint. Dans la réalité, vous décomposerez votre architecture en plusieurs composants (le VPC, la base RDS, la configuration de votre base PostgreSQL, l’outil de SOC) et vous déploierez vos bases dans un réseau privé. Cela étant dit, nous pouvons commencer.
Afin d’illustrer tous ces principes de sécurité, prenons le cas d’une “fake application” de gestion d’un panier, implémentée avec le modèle de données suivant :
L’application possédera 3 composants :
- un composant web permettant la gestion des clients et de leurs paniers,
- un composant backoffice permettant de mettre à jour les produits en vente sur le site,
- et enfin, un batch permettant de mettre à jour une table de statistiques permettant de comptabiliser le montant total vendu par produit. Le traitement sera exécuté par une invraisemblable procédure stockée !
En terme de permissions “nécessaires et suffisantes”, cela donne le tableau suivant :
Composant | CUSTOMER | BASKET | PRODUCT | STATS |
---|---|---|---|---|
Web | Write Operations | Write Operations | Read Operations | N/A |
BackOffice | Read Operations | Read Operations | Write Operations | Read Operations |
Batch(Stats) | Read Operations | Read Operations | Read Operations | Write Operations |
Vous pouvez retrouver l’ensemble du code source ici.
Notez que j’utilise l’excellente librairie direnv
pour positionner un certain nombre de variables d’environnement. Voici mon fichier .envrc :
export PGPASSWORD="My-Strong-Password"
export AWS_PROFILE=ippon-sandbox
export TF_VAR_rds_root_password="${PGPASSWORD}"
- PGPASSWORD : variable d’environnement native de PostgreSQL pour récupérer le mot de passe. C’est comme cela que le provider PostgreSQL fonctionne.
- AWS_PROFILE : Le profil AWS contenant les éléments d’authentification de votre compte AWS.
- TF_VAR_rds_root_password : valorisation de la variable terraform
rds_root_password
Construction de notre “playground”
Comme dit en préambule, on va construire une infrastructure sur AWS. Pour cela, on doit construire en premier lieu un VPC. Vous pouvez retrouver le code dans le fichier vpc.tf.
Notez qu’en plus du vpc, on déclare un security_group
nous permettant de joindre la base de données sur son adresse IP publique, port 5432
, et qui limite l’accès à notre adresse IP de sortie.
Construction de l’instance RDS
Continuons par déployer notre base RDS, en utilisant les modules officiels de la registry terraform :
######################################
# Deploy RDS Instance
######################################
module "rds" {
source = "terraform-aws-modules/rds/aws"
version = "3.5.0"
identifier = local.name
engine = "postgres"
engine_version = var.rds_engine_version
family = var.rds_family
major_engine_version = var.rds_major_engine_version
instance_class = var.rds_instance_class
allocated_storage = var.rds_allocated_storage
max_allocated_storage = var.rds_max_allocated_storage
storage_encrypted = var.rds_storage_encrypted
name = var.inputs["db_name"]
username = var.rds_superuser_name
password = var.rds_root_password
port = 5432
multi_az = true
# because we want reach the database from our local workstation,
# we need to deploy our RDS in the public subnets
# DO NOT DO THAT IN PRODUCTION
# to reduce the attack surface, limit the access of the RDS Instance
# to our personal IP addresses
publicly_accessible = true
subnet_ids = module.vpc.public_subnets
vpc_security_group_ids = [module.security_group.security_group_id]
maintenance_window = "Mon:00:00-Mon:03:00"
backup_window = "03:00-06:00"
enabled_cloudwatch_logs_exports = ["postgresql", "upgrade"]
backup_retention_period = 0
skip_final_snapshot = true
deletion_protection = false
create_db_parameter_group = false
parameter_group_name = aws_db_parameter_group.postgres.id
create_db_option_group = false
create_db_subnet_group = false
db_subnet_group_name = aws_db_subnet_group.main_db_subnet_group.id
tags = local.tags
}
le fichier terraform.tfvars
nous fournit les paramètres :
# rds settings
rds_name = "myfullrdsexample"
rds_engine_version = "13.5"
rds_major_engine_version = "13"
rds_family = "postgres13"
rds_instance_class = "db.t3.micro"
rds_allocated_storage = 10
rds_max_allocated_storage = 20
allowed_ip_addresses = ["X.X.X.X/32"]
rds_superuser_name = "root"
Notez qu’on renomme le superuser en root
et non postgres
(valeur par défaut dans le moteur PostgreSQL).
Construction de la database PostgreSQL
Quelques mots sur 3 attributs relatifs à la ressource terraform aws_db_instance
:
identifier = var.rds_name
name = var.inputs["db_name"]
username = var.rds_superuser_name
- identifier : c’est le nom du moteur postgresql. C’est une enveloppe représentant l’installation d’un serveur postgresql. Il faut le voir comme le nom de la machine sur laquelle est installée le serveur postgresql. Ce n’est pas l’objet “database” au sens postgresql. C’est ce que vous verrez dans le dashboard de synthèse du service RDS dans la console AWS.
- name : c’est le nom de la database au sens postgresql que l’api va nous créer. C’est un attribut optionnel. Si vous ne le spécifiez pas, c’est la database
postgres
qui sera créée. - username : Il s'agit du libellé de l’utilisateur “super-user” associée à votre instance RDS. Si vous ne le spécifiez pas, ce sera le user
postgres
.
Dans notre démonstration, lorsque terraform va nous déployer notre instance RDS, nous aurons donc :
- une instance RDS
myfullrdsexample
- une database
mydatabase
à l’intérieur de cette instance - un user
root
super-user relatif à toutes les databases déployées dans l’instance RDS.
Construction de notre user “admin”
Appelons notre module pour générer les rôles et permissions associées à notre futur utilisateur admin
:
########################################
# Initialize the database and the objects
# (roles & grants), the default privileges
########################################
module "initdb" {
source = "jparnaudeau/database-admin/postgresql//create-database"
version = "2.0.0"
depends_on = [module.rds]
# set the provider
providers = {
postgresql = postgresql.pgadm
}
# targetted rds
pgadmin_user = var.rds_superuser_name
dbhost = module.rds.db_instance_address
dbport = var.dbport
# input parameters for creating database & objects inside database
create_database = false
inputs = var.inputs
# because the superuser is not "postgres", need to set it in the module
default_superusers_list = [var.rds_superuser_name]
}
Remarque : on passe l’attribut create_database
à false
car l’API RDS nous a créé la database pour nous.
Spécifions ensuite ceci dans le fichier terraform.tfvars
:
# database and objects creation
inputs = {
# parameters used for creating database
db_schema_name = "public"
db_name = "mydatabase" # should be the same as var.rds_name. if not, a new database will be created
db_admin = "app_admin_role" # owner of the database
# install extensions if needed
extensions = ["pgaudit"]
# CREATE ROLES
db_roles = [
{ id = "admin", role = "app_admin_role", inherit = true, login = false, validity = "infinity", privileges = ["USAGE", "CREATE"], createrole = true },
],
# GRANT PERMISSIONS ON ROLES
db_grants = [
# define grants for app_admin_role :
# - access to all objects on database
{ object_type = "database", privileges = ["CREATE", "CONNECT", "TEMPORARY"], objects = [], role = "app_admin_role", owner_role = "root", grant_option = true },
{ object_type = "type", privileges = ["USAGE"], objects = [], role = "app_admin_role", owner_role = "root", grant_option = true },
],
# CREATE USERS
db_users = [
{ name = "admin", inherit = true, login = true, membership = ["app_admin_role"], validity = "infinity", connection_limit = -1, createrole = true },
]
}
# Refresh or not refresh passwords
refresh_passwords = ["all"]
Quelques mots sur la structure d’un grant
:
- object_type : la liste des objets supportés par le provider est disponible ici.
- privileges : la liste des privilèges est disponible ici.
- objects : la liste éventuelle des objets sur lesquels le grant s’applique. Liste vide signifie
all
. - role : le rôle sur lequel on souhaite ajouter les permissions.
- owner_role : le rôle à partir duquel le grant est exécuté. Le rôle spécifié doit avoir les permissions suffisantes pour réaliser le grant en question. Ici, parce qu’on est en train de créer l’utilisateur “admin” (il n’existe donc pas encore), on utilise notre super-user “root”.
- grant_option : à
true
, cela signifie qu’on délègue, au rôle qu’on est en train de créer, le droit d’affecter ses privilèges à d’autres “rôles”.
La création des users se fait en utilisant le second sous-module create-users
:
#########################################
# Create the users inside the database
#########################################
# AWS Region
data "aws_region" "current" {}
module "create_users" {
source = "jparnaudeau/database-admin/postgresql//create-users"
version = "2.0.0"
# need that all objects, managed inside the module "initdb", are created
depends_on = [module.initdb]
# set the provider
providers = {
postgresql = postgresql.pgadm
}
# targetted rds
pgadmin_user = var.rds_superuser_name
dbhost = module.rds.db_instance_address
dbport = var.dbport
# input parameters for creating users inside database
db_users = var.inputs["db_users"]
# set passwords
passwords = { for user in var.inputs["db_users"] : user.name => random_password.passwords[user.name].result }
# set postprocessing playbook
postprocessing_playbook_params = {
enable = true
db_name = var.inputs["db_name"]
extra_envs = {
REGION = data.aws_region.current.name
RDS_NAME = var.rds_name
}
refresh_passwords = var.refresh_passwords
shell_name = "./gen-password-in-secretsmanager.py"
}
}
Concernant l’attribut passwords, nous passons des random_password
. On pourrait penser que c’est dangereux car le password correspondant sera stocké en clair dans le remote_state ? Oui mais non.
Ici, on exécute le script gen-password-in-secretsmanager.py. Celui-ci va générer un mot de passe aléatoire, écraser le mot de passe de l’utilisateur créé en base et le stocker dans AWS SecretsManager. Le mot de passe contenu dans le remote_state n’est donc plus l’actuel mot de passe de l’utilisateur.
Le système de postprocessing_playbook injecte automatiquement des variables d’environnement accessibles dans le script :
- Les variables d’environnement natives PostgreSQL : PGHOST, PGPORT, PGUSER, PGDATABASE.
- Une variable d’environnement DBUSER représentant le user pour lequel on souhaite lui affecter un nouveau mot de passe.
- Toutes les variables que vous aurez définies dans
extra_envs
.
Concernant la rotation des mots de passe :
- en laissant
refresh_passwords = ["all"]
, cela indique au post-processing playbook de s’exécuter pour tous les utilisateurs définis. Cela signifie qu’à chaque fois que l’apply est exécuté, les mots de passe des utilisateurs seront redéfinis. - Pour suspendre la rotation des mots de passe, vous pouvez mettre
refresh_passwords = [""]
. - Si vous souhaitez rafraîchir uniquement le mot de passe d'un user, par exemple “redfang”, il suffira alors de mettre
refresh_passwords = ["redfang"]
.
Pour compléter le tout, notons que pour simplifier l’écriture du script, nous créons via terraform des entrées dans AWS SecretsManager. Là aussi, le password qui est utilisé lors de la création de ces secrets n’a pas d’importance. Il sera écrasé lors de l’exécution du post-processing plakbook. Cela évite de gérer dans le playbook le cas où le secret n’existe pas.
A l’apply, si tout se passe bien, on dispose donc d’un utilisateur admin
ayant tous les droits sur la database mydatabase
et dont le mot de passe est récupérable depuis AWS SecretsManager.
Apply complete! Resources: 44 added, 0 changed, 0 destroyed.
Outputs:
affected_schema = "public"
created_roles = [
"app_admin_role",
]
db_users = {
"admin" = {
"connect_command" = "psql -h myfullrdsexample.pandemonium.eu-west-3.rds.amazonaws.com -p 5432 -U admin -d mydatabase -W"
"secret_arn" = "arn:aws:secretsmanager:eu-west-3:444444444444:secret:secret-kv-myfullrdsexample-admin-PFxvvF"
"secret_name" = "secret-kv-myfullrdsexample-admin"
}
}
Tentons de nous connecter :
psql -h myfullrdsexample.pandemonium.eu-west-3.rds.amazonaws.com -p 5432 -U admin -d mydatabase -W
Password: <find password in AWS SecretsManager in secret-kv-myfullrdsexample-admin>
psql (13.5 (Ubuntu 13.5-2.pgdg20.04+1))
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
Type "help" for help.
mydatabase=> \l
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges
------------+----------+----------+-------------+-------------+----------------------------
mydatabase | root | UTF8 | en_US.UTF-8 | en_US.UTF-8 | =Tc/root +
| | | | | root=CTc/root +
| | | | | app_admin_role=C*T*c*/root +
postgres | root | UTF8 | en_US.UTF-8 | en_US.UTF-8 |
rdsadmin | rdsadmin | UTF8 | en_US.UTF-8 | en_US.UTF-8 | rdsadmin=CTc/rdsadmin +
| | | | | rdstopmgr=Tc/rdsadmin
template0 | rdsadmin | UTF8 | en_US.UTF-8 | en_US.UTF-8 | =c/rdsadmin +
| | | | | rdsadmin=CTc/rdsadmin
template1 | root | UTF8 | en_US.UTF-8 | en_US.UTF-8 | =c/root +
| | | | |root=CTc/root
(5 rows)
On vient de terminer les étapes de configuration de notre user admin
. A partir de maintenant, ce sera ce user qui sera utilisé pour les tâches quotidiennes de maintenance de notre base. C'est ce que je vous propose de découvrir dans le prochain article de cette série, où nous déploierons les objets nécessaires à notre "fake application" en adoptant le principe de Least Privilege.
Stay Tuned,