Skip to content

Terraform: Cloud-Native DCS with AWS IAM ABAC

Overview

This Terraform configuration deploys the cloud-native ABAC architecture. Because the authorization logic lives entirely in IAM policies (not Lambda or Verified Permissions), the infrastructure is simpler than the other DCS architectures.

Resource summary

Resource Purpose Count
aws_s3_bucket Data bucket with ABAC bucket policy 1
aws_s3_bucket CloudTrail audit bucket 1
aws_s3_bucket_policy Tag-based ABAC conditions 1
aws_iam_role Data reader role (federated) 1
aws_iam_role Data writer role (federated) 1
aws_iam_role Label admin role (break-glass) 1
aws_iam_role Test user roles (UK, PL, US) 3
aws_cognito_user_pool National IdP simulation 3
aws_cognito_identity_pool Federation to IAM roles 1
aws_cloudtrail S3 data event logging 1
aws_organizations_policy SCPs for guardrails 3

No Lambda functions. No DynamoDB tables. No API Gateway.

Variables

variable "project_name" {
  description = "Project name prefix for all resources"
  type        = string
  default     = "dcs-cloud-native"
}

variable "data_bucket_name" {
  description = "Name of the S3 data bucket"
  type        = string
  default     = "dcs-data"
}

variable "audit_bucket_name" {
  description = "Name of the CloudTrail audit bucket"
  type        = string
  default     = "dcs-audit-trail"
}

variable "nations" {
  description = "Coalition nations with their classification mappings"
  type = map(object({
    clearance_level = number
    nationality     = string
    saps            = list(string)
    organisation    = string
  }))
  default = {
    uk_secret = {
      clearance_level = 2
      nationality     = "GBR"
      saps            = ["WALL"]
      organisation    = "UK-MOD"
    }
    pl_secret = {
      clearance_level = 2
      nationality     = "POL"
      saps            = []
      organisation    = "PL-MON"
    }
    us_secret = {
      clearance_level = 2
      nationality     = "USA"
      saps            = ["WALL"]
      organisation    = "US-DOD"
    }
    contractor = {
      clearance_level = 0
      nationality     = "USA"
      saps            = []
      organisation    = "CONTRACTOR"
    }
  }
}

S3 data bucket

resource "aws_s3_bucket" "data" {
  bucket = var.data_bucket_name

  tags = {
    Project = var.project_name
    Purpose = "DCS labeled data store"
  }
}

resource "aws_s3_bucket_versioning" "data" {
  bucket = aws_s3_bucket.data.id
  versioning_configuration {
    status = "Enabled"
  }
}

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"
    }
    bucket_key_enabled = true
  }
}

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_policy" "dcs_abac" {
  bucket = aws_s3_bucket.data.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid       = "DCSReadAccess"
        Effect    = "Allow"
        Principal = { AWS = aws_iam_role.data_reader.arn }
        Action    = "s3:GetObject"
        Resource  = "${aws_s3_bucket.data.arn}/*"
        Condition = {
          NumericLessThanEquals = {
            "s3:ExistingObjectTag/dcs:classification" = "$${aws:PrincipalTag/dcs:clearance}"
          }
          StringEquals = {
            "s3:ExistingObjectTag/dcs:rel-$${aws:PrincipalTag/dcs:nationality}" = "true"
          }
        }
      },
      {
        Sid       = "DCSOriginatorOverride"
        Effect    = "Allow"
        Principal = { AWS = aws_iam_role.data_reader.arn }
        Action    = "s3:GetObject"
        Resource  = "${aws_s3_bucket.data.arn}/*"
        Condition = {
          StringEquals = {
            "s3:ExistingObjectTag/dcs:originator" = "$${aws:PrincipalTag/dcs:nationality}"
          }
        }
      },
      {
        Sid       = "DCSWriteRequireLabels"
        Effect    = "Allow"
        Principal = { AWS = aws_iam_role.data_writer.arn }
        Action    = "s3:PutObject"
        Resource  = "${aws_s3_bucket.data.arn}/*"
        Condition = {
          StringLike = {
            "s3:RequestObjectTag/dcs:classification" = "*"
          }
          NumericLessThanEquals = {
            "s3:RequestObjectTag/dcs:classification" = "$${aws:PrincipalTag/dcs:clearance}"
          }
        }
      },
      {
        Sid       = "DenyUntaggedUploads"
        Effect    = "Deny"
        Principal = "*"
        Action    = "s3:PutObject"
        Resource  = "${aws_s3_bucket.data.arn}/*"
        Condition = {
          Null = {
            "s3:RequestObjectTag/dcs:classification" = "true"
          }
        }
      },
      {
        Sid    = "DenyTagTampering"
        Effect = "Deny"
        Principal = "*"
        Action = [
          "s3:DeleteObjectTagging",
          "s3:PutObjectTagging"
        ]
        Resource = "${aws_s3_bucket.data.arn}/*"
        Condition = {
          ArnNotEquals = {
            "aws:PrincipalArn" = aws_iam_role.label_admin.arn
          }
        }
      },
      {
        Sid       = "DenySAPMismatch"
        Effect    = "Deny"
        Principal = "*"
        Action    = "s3:GetObject"
        Resource  = "${aws_s3_bucket.data.arn}/*"
        Condition = {
          StringNotEquals = {
            "s3:ExistingObjectTag/dcs:sap"                          = "NONE"
            "s3:ExistingObjectTag/dcs:sap/$${aws:PrincipalTag/dcs:sap}" = ""
          }
        }
      }
    ]
  })
}

IAM roles

# --- Data Reader Role (federated users assume this) ---

resource "aws_iam_role" "data_reader" {
  name = "${var.project_name}-data-reader"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Federated = "cognito-identity.amazonaws.com"
        }
        Action = [
          "sts:AssumeRoleWithWebIdentity",
          "sts:TagSession"
        ]
        Condition = {
          StringEquals = {
            "cognito-identity.amazonaws.com:aud" = aws_cognito_identity_pool.coalition.id
          }
          "ForAllValues:StringLike" = {
            "sts:TransitiveTagKeys" = ["dcs:*"]
          }
        }
      }
    ]
  })

  tags = {
    Project = var.project_name
    Purpose = "DCS data reader with ABAC"
  }
}

resource "aws_iam_role_policy" "data_reader_s3" {
  name = "s3-read-abac"
  role = aws_iam_role.data_reader.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect   = "Allow"
        Action   = ["s3:GetObject", "s3:GetObjectTagging"]
        Resource = "${aws_s3_bucket.data.arn}/*"
      },
      {
        Effect   = "Allow"
        Action   = "s3:ListBucket"
        Resource = aws_s3_bucket.data.arn
      }
    ]
  })
}

# --- Data Writer Role ---

resource "aws_iam_role" "data_writer" {
  name = "${var.project_name}-data-writer"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Federated = "cognito-identity.amazonaws.com"
        }
        Action = [
          "sts:AssumeRoleWithWebIdentity",
          "sts:TagSession"
        ]
        Condition = {
          StringEquals = {
            "cognito-identity.amazonaws.com:aud" = aws_cognito_identity_pool.coalition.id
          }
        }
      }
    ]
  })

  tags = {
    Project = var.project_name
    Purpose = "DCS data writer with label enforcement"
  }
}

resource "aws_iam_role_policy" "data_writer_s3" {
  name = "s3-write-abac"
  role = aws_iam_role.data_writer.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect   = "Allow"
        Action   = ["s3:PutObject", "s3:PutObjectTagging"]
        Resource = "${aws_s3_bucket.data.arn}/*"
      }
    ]
  })
}

# --- Label Admin Role (break-glass only) ---

resource "aws_iam_role" "label_admin" {
  name = "${var.project_name}-label-admin"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
        }
        Action = "sts:AssumeRole"
        Condition = {
          Bool = {
            "aws:MultiFactorAuthPresent" = "true"
          }
        }
      }
    ]
  })

  tags = {
    Project = var.project_name
    Purpose = "DCS label admin - break-glass with MFA"
  }
}

resource "aws_iam_role_policy" "label_admin_s3" {
  name = "s3-tag-management"
  role = aws_iam_role.label_admin.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "s3:PutObjectTagging",
          "s3:GetObjectTagging",
          "s3:DeleteObjectTagging"
        ]
        Resource = "${aws_s3_bucket.data.arn}/*"
      }
    ]
  })
}

data "aws_caller_identity" "current" {}

Cognito identity federation

# --- National Cognito User Pools (simulating national IdPs) ---

resource "aws_cognito_user_pool" "nation" {
  for_each = {
    gbr = { name = "UK", nationality = "GBR" }
    pol = { name = "Poland", nationality = "POL" }
    usa = { name = "US", nationality = "USA" }
  }

  name = "${var.project_name}-${each.key}-users"

  schema {
    name                = "clearance"
    attribute_data_type = "Number"
    mutable             = true
    number_attribute_constraints {
      min_value = "0"
      max_value = "3"
    }
  }

  schema {
    name                = "nationality"
    attribute_data_type = "String"
    mutable             = false
    string_attribute_constraints {
      min_length = "3"
      max_length = "3"
    }
  }

  schema {
    name                = "sap"
    attribute_data_type = "String"
    mutable             = true
    string_attribute_constraints {
      min_length = "0"
      max_length = "256"
    }
  }

  schema {
    name                = "organisation"
    attribute_data_type = "String"
    mutable             = true
    string_attribute_constraints {
      min_length = "0"
      max_length = "256"
    }
  }

  tags = {
    Project = var.project_name
    Nation  = each.value.nationality
  }
}

resource "aws_cognito_user_pool_client" "nation" {
  for_each = aws_cognito_user_pool.nation

  name         = "${each.key}-client"
  user_pool_id = each.value.id

  explicit_auth_flows = [
    "ALLOW_USER_PASSWORD_AUTH",
    "ALLOW_REFRESH_TOKEN_AUTH"
  ]
}

# --- Cognito Identity Pool (federates all nations into IAM roles) ---

resource "aws_cognito_identity_pool" "coalition" {
  identity_pool_name               = "${var.project_name}-coalition"
  allow_unauthenticated_identities = false

  dynamic "cognito_identity_providers" {
    for_each = aws_cognito_user_pool_client.nation
    content {
      client_id               = cognito_identity_providers.value.id
      provider_name           = aws_cognito_user_pool.nation[cognito_identity_providers.key].endpoint
      server_side_token_check = true
    }
  }
}

resource "aws_cognito_identity_pool_roles_attachment" "coalition" {
  identity_pool_id = aws_cognito_identity_pool.coalition.id

  roles = {
    "authenticated" = aws_iam_role.data_reader.arn
  }
}

CloudTrail

resource "aws_s3_bucket" "audit" {
  bucket = var.audit_bucket_name

  tags = {
    Project = var.project_name
    Purpose = "CloudTrail audit logs"
  }
}

resource "aws_s3_bucket_versioning" "audit" {
  bucket = aws_s3_bucket.audit.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_public_access_block" "audit" {
  bucket                  = aws_s3_bucket.audit.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_policy" "audit" {
  bucket = aws_s3_bucket.audit.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid       = "AWSCloudTrailAclCheck"
        Effect    = "Allow"
        Principal = { Service = "cloudtrail.amazonaws.com" }
        Action    = "s3:GetBucketAcl"
        Resource  = aws_s3_bucket.audit.arn
      },
      {
        Sid       = "AWSCloudTrailWrite"
        Effect    = "Allow"
        Principal = { Service = "cloudtrail.amazonaws.com" }
        Action    = "s3:PutObject"
        Resource  = "${aws_s3_bucket.audit.arn}/AWSLogs/*"
        Condition = {
          StringEquals = {
            "s3:x-amz-acl" = "bucket-owner-full-control"
          }
        }
      }
    ]
  })
}

resource "aws_cloudtrail" "dcs" {
  name                       = "${var.project_name}-trail"
  s3_bucket_name             = aws_s3_bucket.audit.id
  include_global_service_events = true
  is_multi_region_trail      = false
  enable_log_file_validation = true

  event_selector {
    read_write_type           = "All"
    include_management_events = true

    data_resource {
      type   = "AWS::S3::Object"
      values = ["${aws_s3_bucket.data.arn}/"]
    }
  }

  tags = {
    Project = var.project_name
    Purpose = "DCS audit trail"
  }
}

Service control policies

# Note: These require AWS Organizations and must be applied from the
# management account. Include them only if deploying in an org context.

resource "aws_organizations_policy" "deny_unlabeled_uploads" {
  name        = "${var.project_name}-deny-unlabeled-uploads"
  description = "Deny S3 PutObject to DCS bucket without classification tags"
  type        = "SERVICE_CONTROL_POLICY"

  content = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid      = "DenyUnlabeledS3Uploads"
        Effect   = "Deny"
        Action   = "s3:PutObject"
        Resource = "arn:aws:s3:::${var.data_bucket_name}/*"
        Condition = {
          Null = {
            "s3:RequestObjectTag/dcs:classification" = "true"
          }
        }
      }
    ]
  })
}

resource "aws_organizations_policy" "deny_label_tampering" {
  name        = "${var.project_name}-deny-label-tampering"
  description = "Deny tag modification on DCS bucket except by label admin"
  type        = "SERVICE_CONTROL_POLICY"

  content = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "DenyLabelDeletion"
        Effect = "Deny"
        Action = [
          "s3:DeleteObjectTagging",
          "s3:PutObjectTagging"
        ]
        Resource = "arn:aws:s3:::${var.data_bucket_name}/*"
        Condition = {
          ArnNotLike = {
            "aws:PrincipalArn" = "arn:aws:iam::*:role/${var.project_name}-label-admin"
          }
        }
      }
    ]
  })
}

resource "aws_organizations_policy" "deny_federation_without_tags" {
  name        = "${var.project_name}-deny-untagged-federation"
  description = "Deny STS federation to DCS roles without clearance session tag"
  type        = "SERVICE_CONTROL_POLICY"

  content = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "DenyFederationWithoutTags"
        Effect = "Deny"
        Action = [
          "sts:AssumeRoleWithSAML",
          "sts:AssumeRoleWithWebIdentity"
        ]
        Resource = "arn:aws:iam::*:role/${var.project_name}-data-*"
        Condition = {
          Null = {
            "aws:RequestTag/dcs:clearance" = "true"
          }
        }
      }
    ]
  })
}

Test users

# Create test users in each national pool for scenario testing

resource "aws_cognito_user" "test_users" {
  for_each = var.nations

  user_pool_id = aws_cognito_user_pool.nation[
    each.value.nationality == "GBR" ? "gbr" :
    each.value.nationality == "POL" ? "pol" : "usa"
  ].id

  username = "test-${each.key}"

  attributes = {
    "custom:clearance"    = tostring(each.value.clearance_level)
    "custom:nationality"  = each.value.nationality
    "custom:sap"          = length(each.value.saps) > 0 ? join(",", each.value.saps) : "NONE"
    "custom:organisation" = each.value.organisation
  }
}

Test data objects

# Upload sample objects with DCS labels for scenario testing

resource "aws_s3_object" "intel_report" {
  bucket       = aws_s3_bucket.data.id
  key          = "intel-report-001.txt"
  content      = "Sample intelligence report - SECRET GBR/USA/POL"
  content_type = "text/plain"

  tags = {
    "dcs:classification"      = "2"
    "dcs:classification-name" = "SECRET"
    "dcs:rel-GBR"             = "true"
    "dcs:rel-USA"             = "true"
    "dcs:rel-POL"             = "true"
    "dcs:sap"                 = "NONE"
    "dcs:originator"          = "POL"
    "dcs:labeled-at"          = timestamp()
  }
}

resource "aws_s3_object" "uk_eyes_only" {
  bucket       = aws_s3_bucket.data.id
  key          = "uk-eyes-only-002.txt"
  content      = "UK EYES ONLY - SECRET assessment"
  content_type = "text/plain"

  tags = {
    "dcs:classification"      = "2"
    "dcs:classification-name" = "SECRET"
    "dcs:rel-GBR"             = "true"
    "dcs:sap"                 = "NONE"
    "dcs:originator"          = "GBR"
    "dcs:labeled-at"          = timestamp()
  }
}

resource "aws_s3_object" "sap_report" {
  bucket       = aws_s3_bucket.data.id
  key          = "wall-report-003.txt"
  content      = "SAP WALL compartmented report"
  content_type = "text/plain"

  tags = {
    "dcs:classification"      = "2"
    "dcs:classification-name" = "SECRET"
    "dcs:rel-GBR"             = "true"
    "dcs:rel-USA"             = "true"
    "dcs:sap"                 = "WALL"
    "dcs:originator"          = "GBR"
    "dcs:labeled-at"          = timestamp()
  }
}

resource "aws_s3_object" "unclass_logistics" {
  bucket       = aws_s3_bucket.data.id
  key          = "logistics-004.csv"
  content      = "Unclassified logistics data"
  content_type = "text/csv"

  tags = {
    "dcs:classification"      = "0"
    "dcs:classification-name" = "UNCLASSIFIED"
    "dcs:rel-ALL"             = "true"
    "dcs:sap"                 = "NONE"
    "dcs:originator"          = "USA"
    "dcs:labeled-at"          = timestamp()
  }
}

Outputs

output "data_bucket_name" {
  value = aws_s3_bucket.data.id
}

output "data_bucket_arn" {
  value = aws_s3_bucket.data.arn
}

output "data_reader_role_arn" {
  value = aws_iam_role.data_reader.arn
}

output "data_writer_role_arn" {
  value = aws_iam_role.data_writer.arn
}

output "label_admin_role_arn" {
  value = aws_iam_role.label_admin.arn
}

output "identity_pool_id" {
  value = aws_cognito_identity_pool.coalition.id
}

output "cognito_user_pools" {
  value = {
    for k, v in aws_cognito_user_pool.nation : k => {
      id       = v.id
      endpoint = v.endpoint
    }
  }
}

output "cloudtrail_arn" {
  value = aws_cloudtrail.dcs.arn
}