WAF-SEC-130 β Data Classification & Protection by Category
Description
All data-storing resources (S3 buckets, RDS instances, DynamoDB tables, ElastiCache clusters) MUST carry a 'data-class' tag indicating the sensitivity level (e.g., public, internal, confidential, pii, health, financial). Resources classified as 'pii', 'health', or 'financial' MUST be encrypted with a customer-managed KMS key (CMK). Database instances MUST have publicly_accessible set to false. S3 buckets MUST have public access blocked via aws_s3_bucket_public_access_block with all four block settings enabled. No data store containing personal data under GDPR scope MUST be directly reachable from the internet.
Rationale
| Risk | Description |
|---|---|
Risk |
Sensitive data is exposed if storage or transport lacks strong encryption. |
Risk |
Compromised keys or weak key rotation extend the blast radius of a breach. |
Requirements
-
All data-storing resources (S3 buckets, RDS instances, DynamoDB tables, ElastiCache clusters) MUST carry a 'data-class' tag indicating the sensitivity level (e.g., public, internal, confidential, pii, health, financial).
-
Resources classified as 'pii', 'health', or 'financial' MUST be encrypted with a customer-managed KMS key (CMK).
-
Database instances MUST have publicly_accessible set to false.
-
S3 buckets MUST have public access blocked via aws_s3_bucket_public_access_block with all four block settings enabled.
-
No data store containing personal data under GDPR scope MUST be directly reachable from the internet.
Implementation Guidance
-
Document the requirement in a version-controlled policy or standard.
-
Assign ownership and establish a review cadence (e.g., quarterly).
-
Implement the required configuration as code in Terraform or equivalent IaC.
Maturity Levels
| Level | Name | Criteria |
|---|---|---|
L1 |
Ad hoc β control not consistently applied |
* Manual evidence exists for some workloads, but coverage is unknown. |
L2 |
Standardised β policy defined and communicated |
* A version-controlled policy, ADR, or standard describes the requirement. |
L3 |
Integrated β automated validation in CI/CD |
* Terraform checks run automatically on pull requests and report results. |
L4 |
Auditable β metrics and exceptions tracked |
* Control coverage, failures, and approved exceptions are reported quarterly. |
L5 |
Optimised β continuous improvement based on data |
* Control effectiveness is measured and iteratively improved. |
Checks
waf-sec-130.tf.aws.data-class-tag-required
Engine: terraform | Provider: aws | Automated: Yes
Data-storing resources must carry a data-class tag
All data-storing resources (S3 buckets, RDS instances, RDS clusters, DynamoDB tables, ElastiCache clusters) must carry a 'data-class' tag indicating the sensitivity level of data stored in the resource. This tag enables automated policy enforcement, access control decisions, and GDPR Art. 30 data inventory generation.
Scope: resource β aws_s3_bucket, aws_db_instance, aws_rds_cluster, aws_dynamodb_table, aws_elasticache_cluster
Assertions:
* tags key exists β Data-storing resource MUST carry a 'data-class' tag (e.g., public, internal, confidential, pii, health, financial).
Remediation: Add a 'data-class' tag to all data-storing resources with a value from the approved classification taxonomy (e.g., "public", "internal", "confidential", "pii", "health", "financial"). Enforce mandatory tagging via an AWS Config rule (required-tags) or an SCP denying CreateBucket, CreateDBInstance, CreateTable API calls without the required tag. Implement a tagging standard in your Terraform module library to make compliant resource creation the default path.
resource "aws_db_instance" "app_db" {
identifier = "prod-application-db"
engine = "postgres"
engine_version = "16.1"
instance_class = "db.t3.medium"
allocated_storage = 100
storage_encrypted = true
kms_key_id = aws_kms_key.rds_pii.arn
publicly_accessible = false
db_subnet_group_name = aws_db_subnet_group.private.name
tags = {
environment = "production"
owner = "application-team"
data-class = "pii" # Required data classification tag
}
}
resource "aws_dynamodb_table" "sessions" {
name = "user-sessions"
billing_mode = "PAY_PER_REQUEST"
hash_key = "session_id"
attribute {
name = "session_id"
type = "S"
}
server_side_encryption {
enabled = true
kms_key_arn = aws_kms_key.dynamodb_internal.arn
}
tags = {
environment = "production"
data-class = "internal"
}
}
resource "aws_db_instance" "app_db" {
identifier = "prod-application-db"
engine = "postgres"
instance_class = "db.t3.medium"
allocated_storage = 100
# No tags at all β WAF-SEC-130 violation: data-class tag required
# No storage_encrypted β encryption required for classified data
# No publicly_accessible = false β WAF-SEC-130 violation (check 2)
}
waf-sec-130.tf.aws.db-not-publicly-accessible
Engine: terraform | Provider: aws | Automated: Yes
Database instances and clusters must not be publicly accessible
All aws_db_instance and aws_rds_cluster resources must have publicly_accessible set to false to prevent direct database access from the internet. Publicly accessible databases expose authentication endpoints to brute-force attacks and vulnerability exploitation and are among the most common causes of catastrophic data breaches.
Scope: resource β aws_db_instance, aws_rds_cluster
Assertions:
* publicly_accessible is false β Database instance or cluster MUST NOT be publicly accessible. Set publicly_accessible = false and place in private subnet.
Remediation: Set publicly_accessible = false on all aws_db_instance and aws_rds_cluster resources. Place database resources in a private subnet group (aws_db_subnet_group) using subnets with no route to an Internet Gateway. Applications must connect to databases via private VPC networking; use AWS Systems Manager Session Manager port forwarding or a bastion host with restricted access for administrative database connections.
resource "aws_db_instance" "main" {
identifier = "prod-db"
engine = "postgres"
engine_version = "16.1"
instance_class = "db.t3.medium"
allocated_storage = 100
storage_encrypted = true
kms_key_id = aws_kms_key.rds.arn
db_subnet_group_name = aws_db_subnet_group.private.name
vpc_security_group_ids = [aws_security_group.rds.id]
publicly_accessible = false
deletion_protection = true
tags = {
environment = "production"
data-class = "confidential"
}
}
resource "aws_db_instance" "main" {
identifier = "prod-db"
engine = "postgres"
instance_class = "db.t3.medium"
allocated_storage = 100
publicly_accessible = true # WAF-SEC-130 critical violation: database exposed to internet
# No subnet group specified β may default to public subnets
# No storage_encrypted β encryption not configured
}
waf-sec-130.tf.aws.s3-public-access-blocked
Engine: terraform | Provider: aws | Automated: Yes
S3 buckets must have all public access block settings enabled
All aws_s3_bucket_public_access_block resources must have block_public_acls, block_public_policy, ignore_public_acls, and restrict_public_buckets all set to true. All four settings are required to fully prevent S3 buckets from being made public through any mechanism including ACLs, bucket policies, or presigned URL configurations.
Scope: resource β aws_s3_bucket_public_access_block
Assertions:
* block_public_acls is true β S3 public access block: block_public_acls must be true.
* block_public_policy is true β S3 public access block: block_public_policy must be true.
* ignore_public_acls is true β S3 public access block: ignore_public_acls must be true.
* restrict_public_buckets is true β S3 public access block: restrict_public_buckets must be true.
Remediation: Create an aws_s3_bucket_public_access_block resource for every aws_s3_bucket resource with all four settings set to true. Additionally, enable the S3 account-level block public access setting via the AWS Console or CLI to provide a safety net for any buckets created outside of Terraform. Use aws_s3_bucket_ownership_controls with object_ownership = "BucketOwnerEnforced" to disable ACL-based access entirely.
resource "aws_s3_bucket" "data" {
bucket = "company-application-data-prod"
tags = {
environment = "production"
data-class = "confidential"
}
}
resource "aws_s3_bucket_public_access_block" "data" {
bucket = aws_s3_bucket.data.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_server_side_encryption_configuration" "data" {
bucket = aws_s3_bucket.data.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.s3.arn
}
bucket_key_enabled = true
}
}
resource "aws_s3_bucket" "data" {
bucket = "company-application-data-prod"
# No aws_s3_bucket_public_access_block β WAF-SEC-130 critical violation
# Bucket defaults may allow public access via ACL or bucket policy
}
resource "aws_s3_bucket_public_access_block" "data_partial" {
bucket = aws_s3_bucket.data.id
block_public_acls = true
block_public_policy = false # WAF-SEC-130 violation: bucket policy can make bucket public
ignore_public_acls = true
restrict_public_buckets = false # WAF-SEC-130 violation: public bucket access not restricted
}
waf-sec-130.tf.azurerm.data-class-tag
Engine: terraform | Provider: azurerm | Automated: Yes
Azure data-storing resources must carry a data-class tag
All Azure resources that store or process data (azurerm_storage_account, azurerm_mssql_database, azurerm_postgresql_server, azurerm_key_vault) must carry a data-class tag with a value from the approved taxonomy.
Scope: resource β azurerm_storage_account, azurerm_mssql_database, azurerm_postgresql_server, azurerm_key_vault
Assertions:
* tags.data-class attribute exists β Azure data-storing resource must carry a data-class tag.
Remediation: Add a 'data-class' tag to all data-storing Azure resources. Use values from the approved classification taxonomy (e.g., 'public', 'internal', 'confidential', 'pii').
resource "azurerm_storage_account" "app" {
name = "appstorage"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
account_tier = "Standard"
account_replication_type = "GRS"
tags = {
data-class = "confidential"
}
}
resource "azurerm_storage_account" "app" {
name = "appstorage"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
account_tier = "Standard"
account_replication_type = "GRS"
# data-class tag missing
}
waf-sec-130.tf.azurerm.sql-no-public-network
Engine: terraform | Provider: azurerm | Automated: Yes
Azure SQL and PostgreSQL servers must disallow public network access
All azurerm_mssql_server and azurerm_postgresql_server resources must set public_network_access_enabled = false.
Scope: resource β azurerm_mssql_server, azurerm_postgresql_server
Assertions:
* public_network_access_enabled is false β Azure SQL/PostgreSQL server must disable public network access.
Remediation: Set public_network_access_enabled = false on every Azure SQL and PostgreSQL server. Connect via private endpoints or VNet service endpoints.
resource "azurerm_mssql_server" "app" {
name = "app-sql"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
version = "12.0"
administrator_login = "sqladmin"
administrator_login_password = var.sql_password
public_network_access_enabled = false
}
resource "azurerm_mssql_server" "app" {
name = "app-sql"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
version = "12.0"
administrator_login = "sqladmin"
administrator_login_password = var.sql_password
# public_network_access_enabled defaults to true
}
waf-sec-130.tf.azurerm.storage-no-public-blob
Engine: terraform | Provider: azurerm | Automated: Yes
Azure Storage accounts must disallow public blob access
All azurerm_storage_account resources must set allow_nested_items_to_public_access = false (or allow_blob_public_access = false depending on provider version) to prevent anonymous blob access.
Scope: resource β azurerm_storage_account
Assertions:
* allow_nested_items_to_public_access is false β Azure Storage account must disallow public blob access.
Remediation: Set allow_nested_items_to_public_access = false on every azurerm_storage_account resource. Review existing containers and blobs for public access and remove it.
resource "azurerm_storage_account" "app" {
name = "appstorage"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
account_tier = "Standard"
account_replication_type = "GRS"
allow_nested_items_to_public_access = false
}
resource "azurerm_storage_account" "app" {
name = "appstorage"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
account_tier = "Standard"
account_replication_type = "GRS"
# allow_nested_items_to_public_access defaults to true
}
waf-sec-130.tf.google.data-class-label
Engine: terraform | Provider: google | Automated: Yes
GCP data-storing resources must carry a data-class label
All GCP resources that store or process data (google_storage_bucket, google_sql_database_instance, google_secret_manager_secret, google_kms_crypto_key) must carry a data-class label.
Scope: resource β google_storage_bucket, google_sql_database_instance, google_secret_manager_secret, google_kms_crypto_key
Assertions:
* labels.data-class attribute exists β GCP data-storing resource must carry a data-class label.
Remediation: Add a 'data-class' label to all GCP data-storing resources. Use values from the approved classification taxonomy.
resource "google_storage_bucket" "app" {
name = "app-bucket"
location = "EU"
force_destroy = false
labels = {
data-class = "confidential"
}
}
resource "google_storage_bucket" "app" {
name = "app-bucket"
location = "EU"
force_destroy = false
# data-class label missing
}
waf-sec-130.tf.google.sql-no-public-ip
Engine: terraform | Provider: google | Automated: Yes
Google Cloud SQL instances must not have a public IP address
All google_sql_database_instance resources must set settings.ip_configuration.ipv4_enabled = false to prevent public internet access.
Scope: resource β google_sql_database_instance
Assertions:
* settings.ip_configuration.ipv4_enabled is false β Cloud SQL instance must disable public IPv4 access.
Remediation: Set ipv4_enabled = false in the ip_configuration block of every Cloud SQL instance. Connect via private IP or Cloud SQL Proxy.
resource "google_sql_database_instance" "main" {
name = "app-db"
database_version = "POSTGRES_15"
region = "europe-west3"
settings {
tier = "db-custom-2-7680"
ip_configuration {
ipv4_enabled = false
private_network = google_compute_network.main.id
}
}
}
resource "google_sql_database_instance" "main" {
name = "app-db"
database_version = "POSTGRES_15"
region = "europe-west3"
settings {
tier = "db-custom-2-7680"
ip_configuration {
# ipv4_enabled defaults to true
}
}
}
waf-sec-130.tf.google.storage-public-access-prevention
Engine: terraform | Provider: google | Automated: Yes
Google Cloud Storage buckets must enforce public access prevention
All google_storage_bucket resources must set public_access_prevention = "enforced" and enable uniform_bucket_level_access to prevent anonymous access via ACLs.
Scope: resource β google_storage_bucket
Assertions:
* public_access_prevention equals enforced β Cloud Storage bucket must enforce public access prevention.
* uniform_bucket_level_access is true β Cloud Storage bucket must use uniform bucket-level access.
Remediation: Set public_access_prevention = "enforced" and uniform_bucket_level_access = true on every google_storage_bucket resource.
resource "google_storage_bucket" "app" {
name = "app-bucket"
location = "EU"
force_destroy = false
uniform_bucket_level_access = true
public_access_prevention = "enforced"
}
resource "google_storage_bucket" "app" {
name = "app-bucket"
location = "EU"
force_destroy = false
# public_access_prevention defaults to "inherited"
}
waf-sec-130.tf.stackit.load-balancer-tls
Engine: terraform | Provider: stackit | Automated: Yes
STACKIT load balancers must enforce TLS 1.2 or higher
stackit_load_balancer resources terminating TLS must use TLS 1.2 or higher with a valid certificate. Weak protocols and ciphers must be disabled.
Scope: resource β stackit_load_balancer
Assertions:
* name attribute exists β STACKIT load balancer must be configured with TLS 1.2+ and a valid certificate.
Remediation: Configure stackit_load_balancer listeners to use TLS 1.2+ and strong cipher suites. Use certificates from a trusted CA or STACKIT-managed certificate service.
resource "stackit_load_balancer" "app" {
project_id = var.stackit_project_id
name = "app-lb"
# listener configured with TLS 1.2+ and valid certificate
}
resource "stackit_load_balancer" "app" {
project_id = var.stackit_project_id
name = "app-lb"
# WAF-SEC-130 violation: TLS 1.0/1.1 or self-signed certificate
}
waf-sec-130.config.ovh.ovh-identity-access
Engine: config | Provider: ovh | Automated: Yes
OVHcloud public endpoints must enforce TLS 1.2 or higher
A version-controlled policy document must exist at docs/security/waf-sec-130-ovh-identity-access.md and document the OVHcloud implementation of this control. The checker validates that the file exists and contains a normative requirement statement (MUST/SHALL/required/mandatory).
Scope: file β docs/security/waf-sec-130-ovh-identity-access.md
Assertions:
* content matches pattern (?i)MUST|SHALL|required|mandatory|enforced|documented β Policy file docs/security/waf-sec-130-ovh-identity-access.md must contain a normative requirement statement.
Remediation: Create or update docs/security/waf-sec-130-ovh-identity-access.md. State the requirement, how it is implemented for OVHcloud, the review cadence, and the owner.
docs/security/waf-sec-130-ovh-identity-access.md:
- The OVHcloud implementation of WAF-SEC-130 is documented.
- Implementation status and owner are recorded.
- Review cadence is defined (e.g. quarterly).
# `docs/security/waf-sec-130-ovh-identity-access.md` is missing or does not contain a normative requirement.
# WAF-SEC-130 violation: OVHcloud control is not documented
waf-sec-130.tf.hcloud.load-balancer-tls
Engine: terraform | Provider: hcloud | Automated: Yes
Hetzner Cloud load balancers must enforce TLS 1.2 or higher
hcloud_load_balancer services terminating TLS must use TLS 1.2 or higher with a valid certificate. Weak protocols and ciphers must be disabled.
Scope: resource β hcloud_load_balancer
Assertions:
* name attribute exists β Hetzner Cloud load balancer must be configured with TLS 1.2+ and a valid certificate.
Remediation: Configure hcloud_load_balancer services to use TLS 1.2+ and upload a valid certificate. Disable weak protocols and ciphers.
resource "hcloud_load_balancer" "app" {
name = "app-lb"
load_balancer_type = "lb11"
location = "fsn1"
}
# hcloud_load_balancer_service configured with TLS 1.2+ certificate
resource "hcloud_load_balancer" "app" {
name = "app-lb"
load_balancer_type = "lb11"
location = "fsn1"
# WAF-SEC-130 violation: TLS 1.0/1.1 or self-signed certificate
}
Evidence
| Kind | Required | Description |
|---|---|---|
|
Required |
Terraform configuration implementing the control. |
|
Required |
Policy, ADR, or standard documenting the requirement. |
|
Optional |
Cloud console or API output confirming runtime state. |
Related Controls
-
WAF-AGN-010 == Regulatory Mapping
| Framework | Controls |
|---|---|
ISO 27001:2022 |
* A.5.12 β Classification of information * A.5.13 β Labelling of information * A.8.10 β Information deletion * A.8.11 β Data masking * A.8.24 β Use of cryptography |
ISO 27017 |
* CLD.8.1 β Asset management in cloud services |
ISO 27018 |
* A.2 β Purpose legitimacy and PII protection * A.10 β Confidentiality and security of PII |
CIS Controls v8 |
* CIS 3 β Data Protection * CIS 3.7 β Establish and maintain a data classification scheme |
CSA STAR |
* CCM DSP-01 β Data Classification * CCM DSP-02 β Data Inventory and Flow * CCM DSP-09 β Data Protection by Design |
PCI DSS v4.0 |
* Req 3 β Protect stored cardholder data |
SOC 2 Type II |
* C1.1 β Confidentiality of information * P1 β Privacy criteria |
GDPR |
* Art. 5(1)(c) β Data minimisation * Art. 5(1)(f) β Integrity and confidentiality * Art. 9 β Special categories of personal data * Art. 25 β Data protection by design and by default * Art. 30 β Records of processing activities * Art. 32 β Security of processing |
NIS2 |
* Art. 21(2)(e) β Security in handling of data |
DORA |
* Art. 9 β Protection and prevention |
EUCS (ENISA) |
* DSP-01 β Data classification and handling * DSP-02 β Data lifecycle management |
BSI C5:2020 |
* AM-04 β Data classification and handling * CRY-03 β Encryption of stored data |
IT-Grundschutz |
* ORP.4 β Schutzbedarfsfeststellung * CON.2 β Datenschutz |
TISAX |
* Information security β Information classification and protection * Prototype protection β Sensitive data handling |
ANSSI SecNumCloud |
* Domain β Data classification and protection |
BIO |
* BIO β Classificatie en bescherming van informatie |
ENS High |
* mp.info.1 β Datos de carΓ‘cter personal * mp.info.2 β CalificaciΓ³n de la informaciΓ³n |
UK NCSC CAF |
* B3.a β Understanding data * B3.b β Data in transit |
UK GDPR |
* Art. 5(1)(f) β Integrity and confidentiality * Art. 25 β Data protection by design * Art. 32 β Security of processing |
NIST SP 800-53 |
* MP-1 β Media Protection Policy * RA-2 β Security Categorization * SC-28 β Protection of Information at Rest |
NIST CSF 2.0 |
* ID.AM-05 β Resources are prioritized based on classification * PR.DS-01 β Data-at-rest is protected |
FedRAMP |
* MP-1, RA-2, SC-28 (Moderate/High baseline) |
HIPAA |
* Β§ 164.312(a)(2)(iv) β Encryption and decryption * Β§ 164.514 β De-identification of PHI |
CMMC 2.0 |
* MP.L2-3.8.1 β Protect CUI on system media * SC.L2-3.13.16 β Protect CUI confidentiality at rest |
IRAP |
* ISM β Information classification and handling |
CCCS PBMM |
* RA-2, SC-28 β Information categorization and protection |
MAS TRM |
* Ch.5 β Information classification and data management |
ISMAP |
* Information classification and protection |
FISC |
* Operational measures β Information classification |