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

  1. Document the requirement in a version-controlled policy or standard.

  2. Assign ownership and establish a review cadence (e.g., quarterly).

  3. 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.

Compliant
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"
  }
}
Non-Compliant
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.

Compliant
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"
  }
}
Non-Compliant
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.

Compliant
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
  }
}
Non-Compliant
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').

Compliant
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"
  }
}
Non-Compliant
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.

Compliant
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
}
Non-Compliant
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.

Compliant
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
}
Non-Compliant
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.

Compliant
resource "google_storage_bucket" "app" {
  name          = "app-bucket"
  location      = "EU"
  force_destroy = false
  labels = {
    data-class = "confidential"
  }
}
Non-Compliant
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.

Compliant
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
    }
  }
}
Non-Compliant
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.

Compliant
resource "google_storage_bucket" "app" {
  name          = "app-bucket"
  location      = "EU"
  force_destroy = false
  uniform_bucket_level_access = true
  public_access_prevention    = "enforced"
}
Non-Compliant
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.

Compliant
resource "stackit_load_balancer" "app" {
  project_id = var.stackit_project_id
  name       = "app-lb"
  # listener configured with TLS 1.2+ and valid certificate
}
Non-Compliant
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.

Compliant
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).
Non-Compliant
# `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.

Compliant
resource "hcloud_load_balancer" "app" {
  name               = "app-lb"
  load_balancer_type = "lb11"
  location           = "fsn1"
}

# hcloud_load_balancer_service configured with TLS 1.2+ certificate
Non-Compliant
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

iac

Required

Terraform configuration implementing the control.

governance

Required

Policy, ADR, or standard documenting the requirement.

config

Optional

Cloud console or API output confirming runtime state.

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