Dans les précédents articles (Part1 & Part2), nous avons abordé les principes de sécurité à mettre en œuvre au sein d'une base PostgreSQL. Nous avons appliqué ces principes en déployant dans une 1ere phase, les roles et les permissions pour créer notre user "admin" de notre base.
Continuons en créant le modèle de données de notre "fake application", ainsi que la procédure stockée.
psql -h myfullrdsexample.pandemonium.eu-west-3.rds.amazonaws.com -p 5432 -U admin -d mydatabase -f create-tables.sql -W
Password: <find password in AWS SecretsManager in secret-kv-myfullrdsexample-admin>
CREATE TABLE
INSERT 0 1
CREATE TABLE
CREATE INDEX
CREATE TABLE
CREATE TABLE
psql -h myfullrdsexample.pandemonium.eu-west-3.rds.amazonaws.com -p 5432 -U admin -d mydatabase -f create-procedure-statistiques.sql -W
CREATE PROCEDURE
psql -h myfullrdsexample.pandemonium.eu-west-3.rds.amazonaws.com -p 5432 -U admin -d mydatabase -f create-tables.sql -W
Password:
mydatabase=> \dt
List of relations
Schema | Name | Type | Owner
--------+----------+-------+-------
public | basket | table | admin
public | customer | table | admin
public | product | table | admin
public | stats | table | admin
(4 rows)
Pour créer des rôles ayant des permissions sur des objets en base, il est nécessaire que ces objets existent. C’est pour cela, qu’on crée d’abord le user “admin”, ensuite le modèle de données et enfin les différents users qui seront utilisés dans les composants de la “fake application”.
Construction de nos users applicatifs avec limitation de leurs permissions
Complétons notre fichier terraform.tfvars
:
La partie “role” :
# CREATE ROLES
db_roles = [
{ id = "admin", role = "app_admin_role", inherit = true, login = false, validity = "infinity", privileges = ["USAGE", "CREATE"], createrole = true },
{ id = "readonly", role = "app_readonly_role", inherit = true, login = false, validity = "infinity", privileges = ["USAGE"], createrole = false },
{ id = "web", role = "app_writeweb_role", inherit = true, login = false, validity = "infinity", privileges = ["USAGE"], createrole = false },
{ id = "backoffice", role = "app_writebo_role", inherit = true, login = false, validity = "infinity", privileges = ["USAGE"], createrole = false },
{ id = "batch", role = "app_writebatch_role", inherit = true, login = false, validity = "infinity", privileges = ["USAGE"], createrole = false },
],
Appliquons les permissions strictement nécessaires pour chacun de ces rôles :
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 },
# define grants for app_readonly_role
# - access to 'SELECT' on all tables
# - access to 'SELECT' on all sequences
{ object_type = "database", privileges = ["CONNECT"], objects = [], role = "app_readonly_role", owner_role = "app_admin_role", grant_option = false },
{ object_type = "type", privileges = ["USAGE"], objects = [], role = "app_readonly_role", owner_role = "app_admin_role", grant_option = true },
{ object_type = "table", privileges = ["SELECT", "REFERENCES", "TRIGGER"], objects = [], role = "app_readonly_role", owner_role = "app_admin_role", grant_option = false },
{ object_type = "sequence", privileges = ["SELECT", "USAGE"], objects = [], role = "app_readonly_role", owner_role = "app_admin_role", grant_option = false },
# define grants for app_writeweb_role
# - access in Read/Write on tables "customer" & "basket"
# - access in Read on table "Product"
{ object_type = "database", privileges = ["CONNECT"], objects = [], role = "app_writeweb_role", owner_role = "app_admin_role", grant_option = false },
{ object_type = "type", privileges = ["USAGE"], objects = [], role = "app_writeweb_role", owner_role = "app_admin_role", grant_option = true },
{ object_type = "table", privileges = ["SELECT", "REFERENCES", "TRIGGER", "INSERT", "UPDATE", "DELETE"], objects = ["customer", "basket"], role = "app_writeweb_role", owner_role = "app_admin_role", grant_option = false },
{ object_type = "table", privileges = ["SELECT", "REFERENCES", "TRIGGER"], objects = ["product"], role = "app_writeweb_role", owner_role = "app_admin_role", grant_option = false },
{ object_type = "sequence", privileges = ["SELECT", "USAGE"], objects = [], role = "app_writeweb_role", owner_role = "app_admin_role", grant_option = false },
# define grants for app_writebo_role
# - access in Read/Write on table "product"
# - access in Read on table "customer" , "basket" & "stats"
{ object_type = "database", privileges = ["CONNECT"], objects = [], role = "app_writebo_role", owner_role = "app_admin_role", grant_option = false },
{ object_type = "type", privileges = ["USAGE"], objects = [], role = "app_writebo_role", owner_role = "app_admin_role", grant_option = true },
{ object_type = "table", privileges = ["SELECT", "REFERENCES", "TRIGGER", "INSERT", "UPDATE", "DELETE"], objects = ["product"], role = "app_writebo_role", owner_role = "app_admin_role", grant_option = false },
{ object_type = "table", privileges = ["SELECT", "REFERENCES", "TRIGGER"], objects = ["customer", "basket", "stats"], role = "app_writebo_role", owner_role = "app_admin_role", grant_option = false },
{ object_type = "sequence", privileges = ["SELECT", "USAGE"], objects = [], role = "app_writebo_role", owner_role = "app_admin_role", grant_option = false },
# define grants for app_writebatch_role
# - access in Read/Write on table "stats"
# - access in Read on table "customer", "basket", "product"
# - execute functions
{ object_type = "database", privileges = ["CONNECT"], objects = [], role = "app_writebatch_role", owner_role = "app_admin_role", grant_option = false },
{ object_type = "type", privileges = ["USAGE"], objects = [], role = "app_writebatch_role", owner_role = "app_admin_role", grant_option = true },
{ object_type = "table", privileges = ["SELECT", "REFERENCES", "TRIGGER", "INSERT", "UPDATE", "DELETE"], objects = ["stats"], role = "app_writebatch_role", owner_role = "app_admin_role", grant_option = false },
{ object_type = "table", privileges = ["SELECT", "REFERENCES", "TRIGGER"], objects = ["customer", "basket", "product"], role = "app_writebatch_role", owner_role = "app_admin_role", grant_option = false },
{ object_type = "sequence", privileges = ["SELECT", "USAGE"], objects = [], role = "app_writebatch_role", owner_role = "app_admin_role", grant_option = false },
{ object_type = "function", privileges = ["EXECUTE"], objects = [], role = "app_writebatch_role", owner_role = "app_admin_role", grant_option = false },
],
Notez que pour tous ces rôles, l’attribut owner_role
a la valeur app_admin_role
(et non le super-user) et que l’attribut grant_option
vaut false
.
Enfin, créons les users correspondant à chacun de ces rôles :
db_users = [
{ name = "admin", inherit = true, login = true, membership = ["app_admin_role"], validity = "infinity", connection_limit = -1, createrole = true },
{ name = "web", inherit = true, login = true, membership = ["app_writeweb_role"], validity = "infinity", connection_limit = -1, createrole = false },
{ name = "backoffice", inherit = true, login = true, membership = ["app_writebo_role"], validity = "infinity", connection_limit = -1, createrole = false },
{ name = "batch", inherit = true, login = true, membership = ["app_writebatch_role"], validity = "infinity", connection_limit = -1, createrole = false },
{ name = "lemmy", inherit = true, login = true, membership = ["app_readonly_role"], validity = "2022-07-31 00:00:00+00", connection_limit = -1, createrole = false },
]
L’attribut createrole = false
empêche l’utilisateur de créer un rôle.
Un utilisateur lemmy
, ayant les droits “read” sur toutes les tables, a été créé avec une date d’expiration.
Et on a terminé !! Après l’apply, voici une partie des outputs :
affected_schema = "public"
created_roles = [
"app_admin_role",
"app_readonly_role",
"app_writeweb_role",
"app_writebo_role",
"app_writebatch_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"
}
"backoffice" = {
"connect_command" = "psql -h myfullrdsexample.pandemonium.eu-west-3.rds.amazonaws.com -p 5432 -U backoffice -d mydatabase -W"
"secret_arn" = "arn:aws:secretsmanager:eu-west-3:444444444444:secret:secret-kv-myfullrdsexample-backoffice-cvPvLW"
"secret_name" = "secret-kv-myfullrdsexample-backoffice"
}
"batch" = {
"connect_command" = "psql -h myfullrdsexample.pandemonium.eu-west-3.rds.amazonaws.com -p 5432 -U batch -d mydatabase -W"
"secret_arn" = "arn:aws:secretsmanager:eu-west-3:444444444444:secret:secret-kv-myfullrdsexample-batch-jrtS2V"
"secret_name" = "secret-kv-myfullrdsexample-batch"
}
"lemmy" = {
"connect_command" = "psql -h myfullrdsexample.pandemonium.eu-west-3.rds.amazonaws.com -p 5432 -U lemmy -d mydatabase -W"
"secret_arn" = "arn:aws:secretsmanager:eu-west-3:444444444444:secret:secret-kv-myfullrdsexample-lemmy-g0ARmI"
"secret_name" = "secret-kv-myfullrdsexample-lemmy"
}
"web" = {
"connect_command" = "psql -h myfullrdsexample.pandemonium.eu-west-3.rds.amazonaws.com -p 5432 -U web -d mydatabase -W"
"secret_arn" = "arn:aws:secretsmanager:eu-west-3:444444444444:secret:secret-kv-myfullrdsexample-web-PvmPYu"
"secret_name" = "secret-kv-myfullrdsexample-web"
}
}
Voici le résultat des tests :
- user “backoffice” :
Operation | Customer | Basket | Product | Stats |
---|---|---|---|---|
Read | OK | OK | OK | OK |
Write | Permission Denied | Permission Denied | OK | Permission Denied |
- user “web” :
Operation | Customer | Basket | Product | Stats |
---|---|---|---|---|
Read | OK | OK | OK | Permission Denied |
Write | OK | OK | Permission Denied | Permission Denied |
- user “batch” :
Operation | Customer | Basket | Product | Stats |
---|---|---|---|---|
Read | OK | OK | OK | OK |
Write | Permission Denied | Permission Denied | Permission Denied | OK |
C’est donc exactement ce que l’on souhaitait.
Testons quelques instructions SQL d’élévation de privilèges depuis le user web
:
psql -h myfullrdsexample.pandemonium.eu-west-3.rds.amazonaws.com -p 5432 -U web -d mydatabase -W
Password:
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)
mydatabase=> CREATE DATABASE exfiltration;
ERROR: permission denied to create database
mydatabase=> CREATE SCHEMA exfiltration;
ERROR: permission denied for database mydatabase
mydatabase=> CALL feed_stats(1);
ERROR: permission denied for table stats
CONTEXT: SQL statement "insert into stats (product,value) values (infos.ProductLabel,infos.totalAmount)"
PL/pgSQL function feed_stats(integer) line 18 at SQL statement
mydatabase=> CREATE ROLE dream SUPERUSER LOGIN PASSWORD 'theater';
ERROR: must be superuser to create superusers
mydatabase=> CREATE ROLE suicidal CREATEDB LOGIN PASSWORD 'tendencies';
ERROR: permission denied to create role
mydatabase=> CREATE ROLE marilyn LOGIN PASSWORD 'manson';
ERROR: permission denied to create role
mydatabase=> CREATE ROLE slayer;
ERROR: permission denied to create role
Ce qui est plutôt rassurant.
Conclusion
Dans cet exemple, si j’avais dû gérer manuellement la création des rôles, des permissions et des users, cela m’aurait pris un certain temps pour adapter les scripts SQL que j’ai amoncelés durant mes différentes expériences. D’autre part, j'aurais dû reporter les mots de passe dans AWS SecretsManager.
Enfin, ici j’ai créé 5 users. Si je devais le faire pour 20, voire plus, comme c’est possible dans certaines organisations, cela prendrait encore plus de temps.
Si on doit rajouter la gestion d’un process pour supprimer un user, ou invalider son accès, ou mettre à jour son mot de passe, ou étendre sa date d’expiration, c’est encore d’autres scripts qu’il faut maintenir.
Avec ce module, j’ai maintenant une méthode simple permettant d’appliquer tous les aspects de sécurité présentés dans le 1er article. Ce module s’inscrit parfaitement dans la mouvance “devops” en apportant aux développeurs ou aux ops, un moyen de sécuriser leurs bases de données.
Enfin, le système de post-processing playbook mis en œuvre permet d’adapter ce module à chaque contexte client, et par la même occasion, de stocker à un seul endroit le mot de passe, dans un endroit sécurisé de son choix. Bien que l’utilisation des null_resource
de Terraform soit à proscrire, en écrivant des scripts shell ou python, vous ne prendrez pas un grand risque dans l’interopérabilité de l’exécution du code Terraform.
Pour terminer le tour d'horizon, retrouver dans le 4eme et dernier article, la mise en oeuvre d'un système d'auditabilité, nécessaire dans certaines activités soumises aux réglementations. Ces entreprises doivent pouvoir prouver l'activité sur certaines applications critiques, et par conséquent sur leurs bases de données.
Stay Tuned,