Category Archives: Compliance

SCP Guardrails That Actually Work in Real AWS Organizations

Service Control Policies are the most powerful preventive control in AWS, and they are responsible for some of the most painful production outages I have seen. The failure mode is always the same: someone writes a policy that looks correct, attaches it to an OU, and then spends three hours at 2 AM figuring out why CloudFormation StackSets, cross-account assumes, or an incident response automation just stopped working. The policy was correct – it just wasn’t precise.

This post is not an introduction to SCPs. If you want the conceptual overview, AWS’s own documentation is adequate. What I want to do here is walk through the non-obvious mechanics, the production failure modes I have personally diagnosed, a tiered strategy that scales across a real organization, and the operational patterns that keep the guardrails from becoming the thing you are guarding against.

One important note before we begin: as of September 2025, AWS expanded SCP syntax to support the full IAM policy language – wildcards at the beginning or middle of Action strings, NotAction in Allow statements, and NotResource. These changes resolve some historical limitations I will point out along the way. And in May 2026, AWS increased the per-node SCP attachment limit from 5 to 10 and the maximum policy size from 5,120 to 10,240 characters – breathing room that reduces the need for the character-compression tricks people previously used.


How SCPs Actually Work (The Parts That Will Surprise You)

The Effective Permissions Formula

Most practitioners understand SCPs as a “ceiling” on permissions, but the precise model matters enormously when you are debugging why something is denied. The effective permissions for any IAM principal in a member account are the intersection of:

  1. What the SCP chain allows (every SCP from root down to the account must allow an action; a deny at any level kills it)
  2. What the identity-based policy grants
  3. What any applicable resource-based policy grants
  4. What any permissions boundary allows (if set)

With Resource Control Policies (RCPs), introduced in November 2024, there is now a fourth axis: RCPs restrict what principals external to your org can do to your resources, independent of SCPs. SCPs and RCPs operate independently – an RCP that blocks cross-account s3:GetObject cannot be overridden by a permissive SCP, and vice versa. If you are not yet using RCPs alongside your SCPs for S3, KMS, Secrets Manager, SQS, and STS, you are missing half the perimeter.

The common misconception is that SCPs grant permissions. They do not. An SCP defines the outer boundary of what is possible for a principal in a member account. The principal still needs an IAM policy that explicitly allows the action. A member account under a permissive FullAWSAccess SCP with an IAM user that has no attached policies has zero effective permissions.

Inheritance: Why Attaching to Root Is Dangerous

The inheritance model is strict: for an action to be allowed, there must be an explicit Allow at every level from root through each OU down to the account. A deny at any single level propagates to everything beneath it. This asymmetry is critical.

When you attach a Deny SCP at root, it affects every account in your organization. When you attach a Deny SCP at the Workloads OU, it affects every account in Workloads. An explicit Deny cascades down; it cannot be overridden by an Allow at a lower level. This is IAM’s fundamental security design.

The implication for root-level attachments is severe: a poorly written Deny SCP at root is an organization-wide incident in the making. I have seen a region restriction SCP attached to root that forgot to exempt IAM from the NotAction list – suddenly, every account lost the ability to create IAM roles, breaking provisioning pipelines and CloudFormation for the entire organization simultaneously.

My rule: attach only absolute prohibitions (Tier 0 in the strategy below) at root. Everything else goes on OUs.

The Management Account Blind Spot

SCPs have no effect on the management account – not on IAM users, IAM roles, or the root user in the management account. This is a hard architectural constraint in AWS, documented and intentional. The reasoning is that the management account needs a recovery path if SCPs are misconfigured.

The consequence is that the management account is an unguarded island. Any principal with access to the management account can do anything in it, regardless of what your SCPs say. This is why landing zone designs – whether you use Control Tower or build your own – push everything except organization management tooling into member accounts.

What do you do about it? Several things:

  • Use the management account for nothing except organization-level operations (account vending, SCP management, consolidated billing). Zero workloads.
  • Apply compensating identity-based controls: tight IAM permission boundaries on every human-assumable role, strict MFA enforcement via policy conditions.
  • Enable CloudTrail in the management account with an organization trail that you cannot disable from member accounts.
  • Alert on every console sign-in to the management account via EventBridge → SNS.

There is no SCP-based solution to this. Accept the constraint and build around it.

Service-Linked Roles: A Frequently Misunderstood Exemption

AWS documentation is unambiguous: SCPs do not restrict service-linked roles. This is stated explicitly in the “Tasks and entities not restricted by SCPs” section of the Organizations documentation. SLRs enable AWS services to act on your behalf – AWSServiceRoleForElasticLoadBalancingAWSServiceRoleForAutoScaling, and similar. They have permissions attached by AWS, not by you, and they operate outside the SCP evaluation path.

Why does this matter? Because when you write a Deny SCP and test whether it is working, you may observe actions succeeding that you expect to be blocked. If an SLR is performing those actions, your SCP is correct – it just does not govern SLRs. This is the most common “my SCP isn’t working” ticket I receive during SCP reviews.

The corollary: you cannot use SCPs to restrict the scope of what a service-linked role can do. If you need to constrain the reach of a specific AWS service integration, you must use resource-based policies and resource control policies – not SCPs.

NotAction in SCPs Is a Footgun at Scale

NotAction with Deny means “deny everything except these actions.” It looks like a convenient shorthand for region restriction and service allowlisting, and Control Tower uses it extensively. The problem emerges at scale.

Every time AWS launches a new service, or a new API action on an existing service, NotAction implicitly allows it. If you use NotAction to define an allowed service list and AWS launches a new service that your security policy prohibits, that new service is automatically reachable in your accounts until someone notices and updates the NotAction list.

The alternative – explicit Action allowlisting in the Effect: Allow statement – is more maintenance-intensive but closes this gap. For services you actively want to prohibit as they become available, use explicit Deny statements for each. For services you want to restrict to specific regions, NotAction with a well-curated list is pragmatic given that the global services exclusion list already needs maintenance anyway.

The new (September 2025) support for NotAction in Allow statements adds another footgun surface. Using Effect: Allow, NotAction: [...] means “allow everything except these listed actions.” This is almost never what you want in an SCP. If you find yourself writing it, step back and consider whether an explicit Deny achieves the same intent with less blast radius.

aws:PrincipalOrgID: Useful, But Not What You Think

aws:PrincipalOrgID lets you scope resource-based policies – S3 bucket policies, KMS key policies, SQS queue policies – to identities within your AWS Organization. It is tremendously useful for preventing data exfiltration to principals outside your org:

{
  "Effect": "Deny",
  "Principal": "*",
  "Action": "s3:GetObject",
  "Resource": "arn:aws:s3:::my-sensitive-bucket/*",
  "Condition": {
    "StringNotEquals": {
      "aws:PrincipalOrgID": "o-exampleorgid11"
    }
  }
}

But aws:PrincipalOrgID is a condition key on resource-based policies and identity-based policies – not an SCP primitive. You cannot use it inside an SCP to scope the SCP itself to specific org IDs (the SCP already applies to your org). Where it does appear in SCPs is in Deny conditions to protect specific resources or in Allow conditions to gate cross-account access, but its most powerful use is in resource-based policies working alongside RCPs.

Do not confuse it with aws:PrincipalOrgPaths, which I will cover in the strategy section.


Common Failure Modes I Have Seen Break Production

Breaking CloudFormation StackSets

AWS CloudFormation StackSets deploy stacks across accounts using the AWSCloudFormationStackSetExecutionRole role in each target account and the AWSCloudFormationStackSetAdministrationRole in the management or delegated admin account. If your SCP includes a broad Deny for cloudformation:* or restricts the IAM permissions that StackSet execution needs to provision resources, StackSets silently fail.

The silent failure is the killer. StackSets return operation-level errors, not SCP-level errors. You will see ACCESS_DENIED on a resource creation inside the stack, trace it back to the execution role, and spend 45 minutes wondering why the execution role’s IAM policy looks fine – before realizing it is the SCP ceiling, not the IAM floor.

Fix: explicitly exempt AWSCloudFormationStackSetExecutionRole and AWSCloudFormationStackSetAdministrationRole from SCPs that restrict IAM or provisioning actions, using aws:PrincipalArn conditions.

Blocking AWS-Managed Provisioning Roles

Similar pattern, broader scope. AWS Control Tower, AWS SSO/IAM Identity Center, and various managed services create provisioning roles in your accounts (AWSControlTowerExecutionAWSReservedSSO_*OrganizationAccountAccessRole). Broad Deny SCPs on iam:* or organizations:* that lack exemptions for these roles will break account provisioning and break-glass access simultaneously.

I have seen an organization deploy an “IAM user creation block” SCP – perfectly reasonable – that used Effect: Deny, Action: iam:CreateUser with no conditions. The SCP worked as intended for human access paths. But it also blocked a Control Tower customization that needed iam:CreateUser as part of its account factory pipeline, because the execution role was not exempted. Account vending broke without a clear error trail.

The Region Restriction SCP That Forgot Global Services

This one is so common it deserves its own entry. The naive region restriction SCP looks like this:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyNonEURegions",
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:RequestedRegion": ["eu-central-1", "eu-west-1"]
        }
      }
    }
  ]
}

Deploy this and you will immediately lose access to IAM, Route53, CloudFront, STS, AWS Support, Billing, Cost Explorer, and a dozen other services whose API endpoints live in us-east-1. These are global services – they do not have regional endpoints and cannot satisfy a region condition pointing to eu-central-1.

The correct pattern uses NotAction to exempt global services from the region restriction, not Action: "*". The AWS Control Tower region deny SCP is the reference implementation and exempts over 60 action namespaces including iam:*sts:*route53:*cloudfront:*kms:*config:*health:*organizations:*billing:*ce:*, and many more. I will provide the full corrected version in the strategy section below.

Locking Out Break-Glass Roles

The scenario: you write a broad Deny at the Workloads OU that restricts sensitive actions for compliance. You do not exempt your emergency access role. An incident hits, someone assumes the break-glass role, and they are blocked by your own SCP from the actions they need to perform containment. Your preventive control has now made your incident response worse.

Every Deny SCP that is broader than a single API action should include a principal exemption:

"Condition": {
  "ArnNotLike": {
    "aws:PrincipalARN": [
      "arn:aws:iam::*:role/BreakGlassRole",
      "arn:aws:iam::*:role/SecurityResponseRole"
    ]
  }
}

The * wildcard in the account ID position is intentional – it exempts the named role pattern across all accounts in the org.

s3:GetObject, SCPs, and the Cross-Account Triangle

S3 access involves three policy documents: the caller’s identity-based policy, the bucket resource-based policy, and the SCP chain governing the caller’s account. If the caller is in account A and the bucket is in account B, the SCP on account A applies to the caller, but the SCP on account B does not apply to the caller (because SCPs only govern principals managed by the attached account).

The confusing scenario: you deny s3:GetObject in your Workloads OU SCP to prevent data egress. A principal in a Workloads account can no longer call s3:GetObject against buckets in the same account – correct. But a principal in the management account (SCP-exempt) calling s3:GetObject against a Workloads-account bucket is not restricted by the Workloads SCP – the SCP does not follow the resource, it follows the principal.

This is exactly the data exfiltration gap that RCPs (Resource Control Policies) close. An RCP attached to the Workloads OU restricts what any principal – including org-external ones – can do to resources in those accounts. If you are using SCPs alone to prevent data exfiltration, you are doing it wrong.

Implicit vs. Explicit: sts:AssumeRole and Cross-Account Trust

For cross-account sts:AssumeRole to work, three things must align:

  1. The calling principal’s identity-based policy must allow sts:AssumeRole for the target role ARN
  2. The target role’s trust policy must allow the calling principal
  3. The SCP on the calling account must allow sts:AssumeRole

Point 3 is where SCPs bite people. If your SCP strategy uses allowlisting (replacing FullAWSAccess with an explicit allow list) and you forget to include sts:AssumeRole in the allowed actions, every cross-account assume – including the ones your CI/CD pipelines depend on – will fail. The error surfaces as AccessDenied on AssumeRole, which looks exactly like a trust policy problem, and engineers chase the wrong thing.

Recommendation: if you use allowlist SCPs anywhere in your org, run IAM Access Analyzer before applying them to verify that all expected cross-account access paths remain valid.


A Tiered SCP Strategy That Scales

The goal of a tiered model is to match SCP severity to attachment point. Absolute prohibitions live at root, baseline controls live on the non-exempt OU set, and workload-specific controls live on specific OUs. The Sandbox OU deliberately opts out of restrictive tiers to allow experimentation.

See the accompanying diagram for the visual representation of this hierarchy.

Tier 0: Absolute Prohibitions (Attached at Root)

Tier 0 SCPs express controls that must hold regardless of account type, workload, or emergency. They have no exemptions except the management account’s inherent SCP exemption. Keep this set small – three to five policies maximum.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyLeaveOrganization",
      "Effect": "Deny",
      "Action": "organizations:LeaveOrganization",
      "Resource": "*"
    },
    {
      "Sid": "DenyRootUserActions",
      "Effect": "Deny",
      "Action": [
        "iam:CreateVirtualMFADevice",
        "iam:DeactivateMFADevice",
        "iam:DeleteVirtualMFADevice",
        "iam:EnableMFADevice",
        "iam:ResyncMFADevice"
      ],
      "Resource": "*",
      "Condition": {
        "StringLike": {
          "aws:PrincipalArn": "arn:aws:iam::*:root"
        }
      }
    },
    {
      "Sid": "DenyS3MFADeleteDisable",
      "Effect": "Deny",
      "Action": "s3:PutBucketVersioning",
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "s3:VersionStatus": "Suspended"
        }
      }
    },
    {
      "Sid": "DenyDisableCloudTrailOrg",
      "Effect": "Deny",
      "Action": [
        "cloudtrail:DeleteTrail",
        "cloudtrail:StopLogging",
        "cloudtrail:UpdateTrail",
        "cloudtrail:DeleteEventDataStore",
        "cloudtrail:UpdateEventDataStore"
      ],
      "Resource": "*",
      "Condition": {
        "ArnNotLike": {
          "aws:PrincipalARN": [
            "arn:aws:iam::*:role/SecurityAuditRole"
          ]
        }
      }
    }
  ]
}

The CloudTrail protection SCP illustrates the pattern: block destructive actions on audit infrastructure with a single exception for the Security team’s audit role. Note that this still belongs at root because CloudTrail protection must cover all accounts.

Tier 1: Baseline Security (Attached to All Non-Exempt OUs)

Tier 1 covers the controls that should apply to every non-sandbox, non-management account. This includes region restriction, public S3 block enforcement, IAM user creation prevention, and – for EU-regulated workloads – explicit blocking of non-EU regions.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyNonEURegionsWithGlobalExemptions",
      "Effect": "Deny",
      "NotAction": [
        "account:*",
        "artifact:*",
        "billing:*",
        "budgets:*",
        "ce:*",
        "cloudfront:*",
        "config:*",
        "cur:*",
        "directconnect:*",
        "fms:*",
        "globalaccelerator:*",
        "health:*",
        "iam:*",
        "invoicing:*",
        "kms:*",
        "networkmanager:*",
        "organizations:*",
        "pricing:*",
        "route53:*",
        "route53domains:*",
        "s3:GetAccountPublicAccessBlock",
        "s3:ListAllMyBuckets",
        "s3:PutAccountPublicAccessBlock",
        "savingsplans:*",
        "shield:*",
        "sso:*",
        "sts:*",
        "support:*",
        "trustedadvisor:*",
        "waf:*",
        "wafv2:*"
      ],
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:RequestedRegion": [
            "eu-central-1",
            "eu-west-1"
          ]
        },
        "ArnNotLike": {
          "aws:PrincipalARN": [
            "arn:aws:iam::*:role/BreakGlassRole",
            "arn:aws:iam::*:role/AWSControlTowerExecution"
          ]
        }
      }
    },
    {
      "Sid": "DenyIAMUserCreation",
      "Effect": "Deny",
      "Action": [
        "iam:CreateUser",
        "iam:CreateAccessKey"
      ],
      "Resource": "*",
      "Condition": {
        "ArnNotLike": {
          "aws:PrincipalARN": [
            "arn:aws:iam::*:role/BreakGlassRole",
            "arn:aws:iam::*:role/AccountVendingRole"
          ]
        }
      }
    },
    {
      "Sid": "DenyPublicS3AccountLevel",
      "Effect": "Deny",
      "Action": "s3:PutAccountPublicAccessBlock",
      "Resource": "*",
      "Condition": {
        "ArnNotLike": {
          "aws:PrincipalARN": "arn:aws:iam::*:role/SecurityAuditRole"
        }
      }
    }
  ]
}

A few notes on this policy:

The NotAction list for the region restriction is the section that will drift most over time. AWS launches new global services regularly. Treat this list as a living document and wire it to a quarterly review process. The Control Tower region deny policy (linked in references) is the canonical AWS-maintained version you should use as the authoritative base, updating it when AWS publishes new revisions.

DenyIAMUserCreation blocks both user creation and access key creation because access keys without IAM users can still be created for existing users. Exempting AccountVendingRole handles the case where your vending pipeline legitimately creates a service account in certain legacy integrations.

DenyPublicS3AccountLevel blocks anyone from disabling the account-level S3 public access block. It does not set the block (that is a separate configuration baseline), but it prevents removal.

Tier 2: Workload-Specific Controls (Attached to Prod OU)

Tier 2 applies to production workload accounts where you want tighter constraints than baseline. The most useful controls here are instance type restrictions, internet gateway blocking for restricted VPCs, and preventing security group rule permissiveness.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyLargeInstanceTypes",
      "Effect": "Deny",
      "Action": "ec2:RunInstances",
      "Resource": "arn:aws:ec2:*:*:instance/*",
      "Condition": {
        "StringNotLike": {
          "ec2:InstanceType": [
            "t3.*",
            "t3a.*",
            "m5.*",
            "m5a.*",
            "m6i.*",
            "m6a.*",
            "c5.*",
            "c6i.*",
            "r5.*",
            "r6i.*"
          ]
        },
        "ArnNotLike": {
          "aws:PrincipalARN": "arn:aws:iam::*:role/MLWorkloadRole"
        }
      }
    },
    {
      "Sid": "DenyInternetGatewayCreation",
      "Effect": "Deny",
      "Action": [
        "ec2:CreateInternetGateway",
        "ec2:AttachInternetGateway"
      ],
      "Resource": "*",
      "Condition": {
        "ArnNotLike": {
          "aws:PrincipalARN": [
            "arn:aws:iam::*:role/NetworkAdminRole",
            "arn:aws:iam::*:role/BreakGlassRole"
          ]
        }
      }
    }
  ]
}

The instance type restriction uses StringNotLike rather than StringEquals for a reason: the former lets you specify families with wildcards (t3.*), while the latter requires exact match on every instance size. With September 2025’s expanded wildcard support in Action strings, keep in mind that the same expansion now applies to condition value comparisons – test carefully.

Sandbox OU: Intentionally Permissive

The Sandbox OU attaches only the FullAWSAccess managed policy plus Tier 0 prohibitions inherited from root. No region restriction, no IAM user block, no instance type limits. Engineers need a place to experiment without filing tickets, and a hardened Sandbox OU is more useful than one with so many guardrails that people work around it using the management account.

What Sandbox is not: a place to store production data, a place to run workloads that access production resources, or a place exempt from security monitoring. GuardDuty, Security Hub, and CloudTrail run identically in Sandbox. The difference is permissive preventive controls, not absent detective controls.


Writing SCPs That Do Not Break Things

The Exemption Pattern

Every non-trivial Deny SCP should follow this structure:

{
  "Effect": "Deny",
  "Action": ["<restricted-action>"],
  "Resource": "*",
  "Condition": {
    "ArnNotLike": {
      "aws:PrincipalARN": [
        "arn:aws:iam::*:role/BreakGlassRole",
        "arn:aws:iam::*:role/<ServiceRole>"
      ]
    }
  }
}

Use ArnNotLike rather than ArnNotEquals because NotLike supports wildcards in the ARN, specifically the * in the account ID position. ArnNotEquals requires an exact ARN match, which means you would need to enumerate every account ID – breaking the exemption whenever a new account is added.

The aws:PrincipalIsAWSService condition key deserves mention here. It resolves to true when the caller is an AWS service principal (e.g., lambda.amazonaws.com calling s3:PutObject on behalf of a function). Adding a condition of "BoolIfExists": {"aws:PrincipalIsAWSService": "false"} to a Deny statement prevents you from accidentally blocking service-to-service calls where a human principal is not involved. This is distinct from service-linked roles (which are entirely SCP-exempt); it covers cases like Lambda, CodePipeline, or Config calling APIs on your behalf through execution roles that are not SLRs.

Using aws:PrincipalOrgPaths for Granular Scoping

When a single SCP needs to apply differently to different parts of the org hierarchy, aws:PrincipalOrgPaths lets you scope a statement to principals in a specific OU path:

{
  "Effect": "Deny",
  "Action": "ec2:RunInstances",
  "Resource": "arn:aws:ec2:*:*:instance/*",
  "Condition": {
    "ForAnyValue:StringLike": {
      "aws:PrincipalOrgPaths": [
        "o-exampleorgid11/r-ab12/ou-ab12-22222222/*"
      ]
    },
    "StringNotLike": {
      "ec2:InstanceType": "t3.*"
    }
  }
}

This is useful when you want a single Deny SCP attached to root (for governance visibility) but scoped to apply only to principals in a specific OU subtree. It reduces SCP fragmentation at the cost of more complex condition expressions. Use it sparingly – the readability trade-off is real.

Testing Before You Ship

The IAM Policy Simulator does not evaluate SCPs directly. To test SCP effects, use IAM Access Analyzer with the ValidatePolicy API or the Organizations console’s SCP simulation. The most reliable approach remains creating a test OU, moving a non-production account into it, attaching the SCP, and running the actual API calls you expect to be allowed and denied.

AWS CLI for quick validation:

# List SCPs attached to an OU
aws organizations list-policies-for-target \
  --target-id ou-xxxx-yyyyyyyy \
  --filter SERVICE_CONTROL_POLICY

# Simulate effective permissions (requires delegated admin or management account)
aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::123456789012:role/TestRole \
  --action-names s3:GetObject ec2:RunInstances sts:AssumeRole \
  --resource-arns "*"

Note that simulate-principal-policy does evaluate SCPs when you call it from the management account or from a delegated admin account with the right permissions. From within a member account, it cannot evaluate org-level SCPs and will give you misleadingly permissive results.


Operational Patterns

The Break-Glass SCP Exception

The break-glass role is a pre-provisioned IAM role in each member account (or assumed from the management account) with broad permissions, zero standing access, and aggressive alerting on assumption. Its existence inside your SCP exception list must be documented and controlled.

The risk: if BreakGlassRole is exempt from your SCPs and someone can assume it without triggering alerts, your entire SCP estate is porous. Protect the exemption:

  1. The BreakGlassRole trust policy permits assumption only from the management account root or a specific, MFA-enforced federated role.
  2. An EventBridge rule fires on every sts:AssumeRole event where requestParameters.roleArn contains BreakGlassRole, triggering an immediate PagerDuty/SNS alert.
  3. CloudTrail logs for break-glass assumptions are shipped to a security account that the role itself cannot write to.
  4. Sessions created via break-glass have a maximum duration of one hour and are tagged with aws:PrincipalTag/BreakGlass: true for audit correlation.
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::MANAGEMENT_ACCOUNT_ID:root"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "Bool": {
          "aws:MultiFactorAuthPresent": "true"
        },
        "NumericLessThan": {
          "aws:MultiFactorAuthAge": "300"
        }
      }
    }
  ]
}

The aws:MultiFactorAuthAge condition (maximum 300 seconds, or 5 minutes) prevents someone from using a stale MFA session – they must have authenticated within the last 5 minutes to assume break-glass.

Proactive SCP Violation Detection

SCPs fail silently from a monitoring perspective – an AccessDenied from an SCP looks identical to an AccessDenied from a missing IAM policy. The error response will say User: arn:aws:iam::123456789012:user/alice is not authorized to perform: s3:GetObject on resource: ... with an explicit deny.

To distinguish SCP denials from IAM denials, parse CloudTrail events for errorCode: AccessDenied with errorMessage containing explicit deny AND cross-reference against your known Deny SCPs. A Denial from an SCP will originate from a policy attached to the org hierarchy, not from an identity-based policy – IAM Access Analyzer can help correlate.

More operationally useful: an EventBridge rule that fires on AccessDenied events for specific high-value API calls (e.g., cloudtrail:DeleteTrailorganizations:LeaveOrganizationiam:DeletePolicy) gives you real-time visibility into SCP effectiveness. These are exactly the actions your Tier 0 SCPs protect, so an AccessDenied on them is proof the guardrail is working – and worth alerting on regardless.

{
  "source": ["aws.cloudtrail"],
  "detail-type": ["AWS API Call via CloudTrail"],
  "detail": {
    "errorCode": ["AccessDenied"],
    "eventName": [
      "DeleteTrail",
      "StopLogging",
      "LeaveOrganization",
      "DeleteOrganization",
      "DetachPolicy",
      "DisablePolicyType"
    ]
  }
}

Route this to an SNS topic with email and Slack integration. Every hit is a signal that someone tried to violate a Tier 0 control.

The Immutable Audit Trail Pattern

SCPs cannot protect the management account. But you can protect the audit logging infrastructure itself.

The pattern:

  1. Create a Security OU containing two accounts: a log archive account and a security tooling account.
  2. The log archive account receives all CloudTrail, Config, and VPC Flow Logs from the org via organization trails and AWS Config aggregation. Its S3 buckets have Object Lock enabled with Compliance mode.
  3. Attach an SCP to the Security OU that prevents deletion of S3 buckets in the log archive account, prevents disabling Object Lock, and prevents modification of the org trail configuration.
  4. The security tooling account runs GuardDuty (delegated admin), Security Hub (delegated admin), and Access Analyzer. The SCP for the Security OU prevents disabling any of these.
{
  "Sid": "ProtectSecurityTooling",
  "Effect": "Deny",
  "Action": [
    "guardduty:DisassociateFromMasterAccount",
    "guardduty:DeleteDetector",
    "guardduty:StopMonitoringMembers",
    "securityhub:DisableSecurityHub",
    "securityhub:DisassociateFromMasterAccount",
    "config:DeleteConfigurationRecorder",
    "config:StopConfigurationRecorder",
    "config:DeleteDeliveryChannel"
  ],
  "Resource": "*",
  "Condition": {
    "ArnNotLike": {
      "aws:PrincipalARN": "arn:aws:iam::*:role/SecurityAuditRole"
    }
  }
}

This SCP is attached to the Security OU itself – it protects the security account from being tampered with even if an attacker gains access to a security tooling account’s IAM credentials. Combined with Object Lock on S3, it provides a reasonably tamper-resistant audit foundation.

Documenting Intent with Tagging and SIDs

Use Sid fields as documentation. A poorly named Sid like "Stmt1" is useless when you are triaging an AccessDenied at 3 AM. Use descriptive SIDs: DenyNonEURegionsWithBreakGlassExemptionTier0DenyLeaveOrgTier1BlockIAMUserCreation.

At the SCP document level, AWS does not support native tagging of SCP policies – frustratingly. The workaround is encoding metadata in the SCP name and description fields via the Organizations API, and maintaining a Terraform module that maps SCP names to their owner, tier, attachment targets, and last-reviewed date. When you have 30+ SCPs across a large org, unowned SCPs become a liability fast.


What I Would Do Differently

Every organization I have worked with that built SCP guardrails from scratch made the same sequence of mistakes: start with too many Deny statements at root, forget global services in region restriction, have no break-glass exemption strategy, and have no automated detection of SCP enforcement. The retrospective always includes a postmortem where the SCP that was supposed to protect something instead blocked incident response.

The structural insight is this: SCPs are write-once, deploy-everywhere controls in a system that is also your recovery path when things go wrong. Before you attach anything to root, ask: “if this SCP had a bug, how would I recover?” If the answer is “I cannot,” the SCP belongs on an OU, not on root.

With the September 2025 syntax expansions and the May 2026 quota increases, there is now more room to write precise, legible SCPs without the character-compression gymnastics of the past. Use that room. An SCP that is easy to read is an SCP that is easy to audit, easy to update when it breaks something, and easy to explain to a compliance officer.


References

KRITIS and NIS2 Compliance on AWS: A Technical Implementation Guide

Germany’s energy sector got a rude awakening in February 2022 when the Rosneft Deutschland oil subsidiary – operator of refineries supplying roughly 12% of German fuel capacity – suffered a cyberattack that took down IT systems and disrupted supply chain visibility for weeks. The attackers had been inside the network for months. The incident triggered a formal BSI KRITIS notification under § 8b BSIG and illustrated exactly the gap that NIS2 was designed to close: critical infrastructure operators with sophisticated physical security and negligible cyber maturity, running IT architectures that no serious security team would have approved in 2015.

If you operate critical infrastructure in Germany, or run digital services that touch essential service operators, you are now subject to two overlapping regulatory frameworks: the German KRITIS regulation (the critical infrastructure provisions of the BSIG – Gesetz über das Bundesamt für Sicherheit in der Informationstechnik) and the EU NIS2 Directive (2022/2555, which replaces the original NIS Directive 2016/1148). Both are in force. Both carry material penalties. And unlike GDPR, where enforcement was slow to start, the BSI has been actively issuing compliance orders and escalating to fines for KRITIS-regulated entities that fail to demonstrate adequate technical measures.

This post documents how to implement the required controls using AWS-native services – not because AWS is the only valid answer, but because it is the platform I have done this on, and the mapping between regulatory obligations and AWS service capabilities is both specific and non-obvious enough to be worth documenting in full.

The Regulatory Landscape: What You Are Actually Dealing With

NIS2: The EU Baseline

NIS2 entered into force in January 2023. Member states had until 17 October 2024 to transpose it into national law. Germany missed that deadline – the domestic political calendar disrupted the legislative process and the draft NIS2UmsuCG stalled in the Bundestag. The European Commission issued a reasoned opinion against Germany on 7 May 2025, the formal step before infringement proceedings. The NIS2UmsuCG (NIS-2-Umsetzungs- und Cybersicherheitsstärkungsgesetz) was eventually passed by the Bundestag on 13 November 2025, amending the BSIG and several related statutes. The amended BSIG came into force on 6 December 2025. The BSI’s reporting portal went live on 6 January 2026, and the registration deadline for newly in-scope entities was 6 March 2026 – giving the roughly 29,500 entities newly captured by the expanded scope less than three months to register. If you read earlier analyses (including a previous version of this post) that placed transposition in “late 2024”, that timeline was the target; the actual German implementation landed more than a year late.

NIS2 creates two tiers of regulated entities:

  • Essential entities (EE): Energy, transport, banking, financial market infrastructure, health, drinking water, wastewater, digital infrastructure (IXPs, DNS providers, TLD registries, cloud providers, data centre operators, CDN providers, managed service providers, managed security service providers), public administration, and space. Thresholds: medium or large enterprises (≥50 employees or ≥€10M turnover) operating in these sectors.
  • Important entities (IE): Postal and courier services, waste management, chemicals manufacturing, food production, manufacturing of medical devices/computers/electronics/machinery/motor vehicles, digital providers (online marketplaces, search engines, social networks), and research organisations. Same size thresholds apply.

The practical distinction matters: essential entities face stricter supervision, mandatory incident notifications with tighter timelines, and higher maximum fines.

Article 21 is the core technical obligations article. It requires entities to implement “appropriate and proportionate technical, operational and organisational measures” across ten specific domains:

  1. Risk analysis and information system security policies
  2. Incident handling
  3. Business continuity (backup management, disaster recovery, crisis management)
  4. Supply chain security (including security in supplier and service provider relationships)
  5. Security in network and information systems acquisition, development and maintenance (including vulnerability handling and disclosure)
  6. Policies and procedures to assess the effectiveness of cybersecurity risk-management measures
  7. Basic cyber hygiene practices and cybersecurity training
  8. Policies and procedures on cryptography and, where appropriate, encryption
  9. Human resources security, access control policies and asset management
  10. Multi-factor authentication or continuous authentication solutions

Article 23 mandates incident notification:

  • Early warning to the national CSIRT (BSI in Germany) within 24 hours of becoming aware of a significant incident
  • Incident notification with initial assessment within 72 hours
  • Intermediate report (for ongoing incidents)
  • Final report within one month of incident notification

A “significant incident” is one that has caused or is capable of causing severe operational disruption, financial loss, or impact on other persons. The BSI has published guidance indicating that any incident affecting the availability or integrity of essential services qualifies.

Penalties under NIS2 / NIS2UmsuCG:

  • Essential entities: up to €10 million or 2% of global annual turnover, whichever is higher
  • Important entities: up to €7 million or 1.4% of global annual turnover
  • Management liability: Directors and senior management can be held personally liable for non-compliance – a provision that has no equivalent in GDPR.

KRITIS: The German Layer

KRITIS is the set of obligations in the BSIG (primarily §§ 8a–8f) that apply to operators of critical infrastructure – a definition distinct from NIS2’s “essential entities,” though there is substantial overlap.

The BSI’s KRITIS regulation (BSI-KritisV) sets sector-specific thresholds based on service delivery capacity. For example:

  • Energy: Operators of electricity generation/distribution above 420 MW installed capacity; natural gas supply above 1,580 MW; oil supply above 420 MW
  • Water: Drinking water supply to more than 500,000 people
  • Health: Hospitals with more than 30,000 inpatient cases per year; pharmaceutical manufacturers above defined production thresholds
  • Digital infrastructure: Internet exchange points with more than 1 Tbps throughput; DNS operators; PKI providers; data centres above 5 MW IT load

KRITIS operators face obligations beyond NIS2:

  • Must implement state-of-the-art technical and organisational measures (§ 8a BSIG) – verified against BSI’s own published standards and the BSI IT-Grundschutz compendium
  • Must audit and demonstrate compliance every two years, submitting evidence to the BSI (§ 8a(3) BSIG) – this is active auditing, not self-certification
  • Must register with the BSI and designate a point-of-contact available 24/7 (§ 8b BSIG)
  • Must report significant incidents to the BSI, initially anonymously if desired, within defined timeframes
  • Sanctions: fines up to €20 million for KRITIS-specific obligations under the amended BSIG

The BSI C5 Testat (Cloud Computing Compliance Criteria Catalogue) is the BSI’s cloud-specific audit framework. AWS holds a C5 Testat for its Frankfurt and Ireland regions, which you can download from AWS Artifact. This covers AWS’s side of the shared responsibility model – your workloads are your problem.

The relationship between the two frameworks is: NIS2 establishes the EU-wide floor; KRITIS extends that floor for the subset of operators that meet the size thresholds in the BSI-KritisV. Most KRITIS operators are also NIS2 essential entities. The applicable obligations are the union of both sets, and where they conflict, the stricter obligation applies.


Control Domain Mapping

Before diving into the AWS implementation, let me be explicit about what the regulatory frameworks actually require at the control level. The following maps NIS2 Article 21 obligations and KRITIS § 8a requirements to concrete control domains, then maps those to AWS services.

Risk Management and Asset Inventory

What NIS2/KRITIS require: A maintained inventory of information assets, regular risk assessments, documented security policies, and evidence that risks drive control selection.

AWS has no native “asset inventory” product, but you can build one from AWS Config and Systems Manager:

# Enable AWS Config in all accounts via Organizations
aws organizations enable-aws-service-access \
  --service-principal config.amazonaws.com

# Create a conformance pack that enforces REQUIRED_TAGS rule
# (forces asset classification tagging on all resources)
aws configservice put-conformance-pack \
  --conformance-pack-name "kritis-asset-tagging" \
  --template-s3-uri "s3://your-config-bucket/kritis-conformance-pack.yaml"

The Config conformance pack below enforces the tagging taxonomy required for an accurate asset register. KRITIS auditors expect resources to be classified by criticality, data classification, and owning business unit:

# kritis-conformance-pack.yaml (excerpt)
Resources:
  RequiredTagsRule:
    Type: AWS::Config::ConfigRule
    Properties:
      ConfigRuleName: required-tags-kritis
      Source:
        Owner: AWS
        SourceIdentifier: REQUIRED_TAGS
      InputParameters:
        tag1Key: DataClassification
        tag1Value: PUBLIC,INTERNAL,CONFIDENTIAL,RESTRICTED
        tag2Key: CriticalityTier
        tag2Value: KRITIS,HIGH,MEDIUM,LOW
        tag3Key: BusinessOwner
        tag4Key: ComplianceScope
        tag4Value: NIS2,KRITIS,BOTH,OUT-OF-SCOPE

Systems Manager Inventory gives you OS-level visibility – installed software, running processes, network configuration – which feeds into the asset register and is required for the vulnerability management programme:

# Query all instances for software inventory via SSM
aws ssm list-inventory-entries \
  --instance-id i-0abc123def456789 \
  --type-name "AWS:Application" \
  --query 'Entries[].{Name:Name,Version:Version}' \
  --output table

For the formal risk register, AWS Audit Manager lets you build a custom assessment framework that maps control objectives to AWS Config rules, CloudTrail events, and Security Hub findings, generating continuous evidence that risk assessments drive control decisions.

Incident Detection and Response

What NIS2/KRITIS require: Continuous monitoring capabilities, detection of security events, and a documented incident response process with the ability to notify the BSI within 24 hours.

The detection stack I build on AWS for KRITIS-scoped environments has three components that must all be active:

GuardDuty is the baseline. Enable it across all accounts via Organizations and ensure all three data source categories are active – CloudTrail management events, S3 data events, and DNS query logs. For Kubernetes workloads, enable EKS Runtime Monitoring. For EC2 workloads, deploy the GuardDuty agent. The default 90-day finding retention is insufficient for KRITIS audit purposes – configure findings to flow to a Security Hub in a dedicated Security account.

# Terraform: enable GuardDuty org-wide with all data sources
resource "aws_guardduty_detector" "main" {
  enable = true

  datasources {
    s3_logs {
      enable = true
    }
    kubernetes {
      audit_logs {
        enable = true
      }
    }
    malware_protection {
      scan_ec2_instance_with_findings {
        ebs_volumes {
          enable = true
        }
      }
    }
  }

  finding_publishing_frequency = "FIFTEEN_MINUTES"
}

resource "aws_guardduty_organization_configuration" "auto_enable" {
  auto_enable_organization_members = "ALL"
  detector_id                      = aws_guardduty_detector.main.id

  datasources {
    s3_logs {
      auto_enable = true
    }
    kubernetes {
      audit_logs {
        enable = true
      }
    }
    malware_protection {
      scan_ec2_instance_with_findings {
        ebs_volumes {
          auto_enable = true
        }
      }
    }
  }
}

Security Hub aggregates findings from GuardDuty, Inspector, Macie, Config, and third-party tools into a single pane. Enable the CIS AWS Foundations Benchmark standard (v1.4 or v3.0) and the AWS Foundational Security Best Practices standard. Both are mapped to NIS2 Article 21 obligations in AWS’s published compliance mapping document, available from AWS Artifact.

The critical Security Hub configuration for KRITIS environments is enabling finding aggregation across all regions into a single aggregation region (eu-central-1 for Germany-primary deployments):

resource "aws_securityhub_finding_aggregator" "central" {
  provider     = aws.security_account
  linking_mode = "ALL_REGIONS"
}

# Enable both standards in every account
resource "aws_securityhub_standards_subscription" "cis" {
  standards_arn = "arn:aws:securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.4.0"
}

resource "aws_securityhub_standards_subscription" "fsbp" {
  standards_arn = "arn:aws:securityhub:eu-central-1::standards/aws-foundational-security-best-practices/v/1.0.0"
}

Business Continuity and Disaster Recovery

What NIS2/KRITIS require: Documented RTO/RPO objectives, tested backup procedures, and crisis management capability. For KRITIS operators, availability guarantees are a legal obligation – the BSI can require specific RTO targets.

AWS Backup provides centralised backup management across EC2, EBS, RDS, DynamoDB, EFS, FSx, and S3. For KRITIS environments, configure backup plans with cross-region copies to eu-west-1 (Ireland) as the DR region:

resource "aws_backup_plan" "kritis_critical" {
  name = "kritis-critical-tier"

  rule {
    rule_name         = "daily-backup-critical"
    target_vault_name = aws_backup_vault.primary.name
    schedule          = "cron(0 2 * * ? *)"
    start_window      = 60
    completion_window = 180

    lifecycle {
      cold_storage_after = 30
      delete_after       = 2557  # 7 years (KRITIS audit retention)
    }

    copy_action {
      destination_vault_arn = aws_backup_vault.dr_region.arn

      lifecycle {
        cold_storage_after = 30
        delete_after       = 2557
      }
    }
  }

  # Continuous backup for point-in-time recovery (RDS)
  rule {
    rule_name         = "continuous-pitr"
    target_vault_name = aws_backup_vault.primary.name
    schedule          = "cron(0 * * * ? *)"
    enable_continuous_backup = true
  }
}

resource "aws_backup_vault_lock_configuration" "kritis" {
  backup_vault_name   = aws_backup_vault.primary.name
  changeable_for_days = 3
  max_retention_days  = 2557
  min_retention_days  = 7
}

The aws_backup_vault_lock_configuration resource enables Vault Lock – WORM protection for backup data that prevents any principal, including the root account, from deleting backups before the minimum retention period. This is a hard requirement when auditors need to verify that backup integrity was maintained.

For DR testing, document actual RTO measurements. BSI auditors will ask for evidence of tested DR procedures, not just documented procedures. Automate DR drills with AWS Fault Injection Simulator (FIS) and capture the results as Audit Manager evidence.

Supply Chain Security

What NIS2/KRITIS require: Assessment of security risks in the supply chain, including software supply chain risks. Article 21(2)(d) explicitly requires entities to address security in supplier and third-party service provider relationships.

The software supply chain controls in an AWS environment focus on three areas:

Container image integrity: Use Amazon ECR with image scanning enabled (both basic scanning for OS CVEs and enhanced scanning powered by Inspector). Enforce signed images using AWS Signer and OPA/Gatekeeper policies in EKS that reject unsigned images:

# Configure ECR enhanced scanning on push
aws ecr put-registry-scanning-configuration \
  --scan-type ENHANCED \
  --rules '[{"repositoryFilters":[{"filter":"*","filterType":"WILDCARD"}],"scanFrequency":"CONTINUOUS_SCAN"}]'

# Generate SBOM for an ECR image (Inspector exports to S3)
aws inspector2 create-sbom-export \
  --resource-filter-criteria '{"ecrImageTags":[{"comparison":"EQUALS","value":"prod"}]}' \
  --report-format CYCLONE_DX_1_4 \
  --s3-destination '{"bucketName":"sbom-archive","keyPrefix":"2026/05/"}'

Package dependency management: Route all package manager traffic through AWS CodeArtifact. This gives you a proxy that caches approved packages, blocks typosquatting attacks, and lets you enforce version pinning for KRITIS-critical services:

# Create a CodeArtifact upstream proxy for PyPI
aws codeartifact create-repository \
  --domain kritis-domain \
  --repository pypi-proxy \
  --upstreams '[]'

aws codeartifact associate-external-connection \
  --domain kritis-domain \
  --repository pypi-proxy \
  --external-connection public:pypi

Third-party vendor assessment: Build a supplier security questionnaire process in Audit Manager. Map your critical suppliers (cloud sub-processors, software vendors with privileged access) to custom controls, and use Audit Manager’s evidence collection to track questionnaire responses and annual assessments. NIS2 Art. 21(2)(d) requires you to document these assessments – Audit Manager gives you a structured, auditable record.

Access Control and Identity Management

What NIS2/KRITIS require: Access control policies, MFA for all privileged access, and (for KRITIS) privileged access management. Article 21(2)(i) explicitly mentions MFA and continuous authentication.

The identity architecture for KRITIS environments should be built on three layers:

AWS Organizations + Service Control Policies (SCPs): SCPs are the last line of defence against insider threats and compromised management accounts. They operate on every API call regardless of identity – you cannot grant a permission that violates an SCP even with AdministratorAccess. Critical SCPs for KRITIS compliance:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyDisableCloudTrail",
      "Effect": "Deny",
      "Action": [
        "cloudtrail:DeleteTrail",
        "cloudtrail:StopLogging",
        "cloudtrail:UpdateTrail"
      ],
      "Resource": "*"
    },
    {
      "Sid": "EnforceEUDataResidency",
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:RequestedRegion": [
            "eu-central-1",
            "eu-west-1",
            "eu-west-2",
            "eu-west-3",
            "eu-north-1",
            "eu-south-1"
          ]
        }
      }
    },
    {
      "Sid": "DenyLeaveOrganization",
      "Effect": "Deny",
      "Action": [
        "organizations:LeaveOrganization"
      ],
      "Resource": "*"
    },
    {
      "Sid": "RequireMFAForSensitiveActions",
      "Effect": "Deny",
      "Action": [
        "iam:DeleteRole",
        "iam:DeletePolicy",
        "iam:AttachRolePolicy",
        "kms:ScheduleKeyDeletion",
        "kms:DisableKey"
      ],
      "Resource": "*",
      "Condition": {
        "BoolIfExists": {
          "aws:MultiFactorAuthPresent": "false"
        }
      }
    }
  ]
}

The EnforceEUDataResidency SCP is critical for GDPR compliance (data residency) and for KRITIS operators whose authorisation to use cloud infrastructure may be conditioned on EU data residency. The list of EU regions is exhaustive as of 2026 – verify this against AWS’s current region list when implementing.

IAM Identity Center with phishing-resistant MFA: Configure IAM Identity Center (formerly AWS SSO) as the single entry point for all human access. Integrate with your corporate IdP (Okta, Azure AD, or similar) via SAML 2.0 or SCIM. Enforce phishing-resistant MFA at the Identity Center level – FIDO2 security keys (YubiKey, etc.) not TOTP – for all KRITIS-scoped accounts.

IAM Access Analyzer is your continuous least-privilege enforcement tool. Run it in all accounts and in your Organizations management account. The external access analyser flags resource policies (S3, KMS, IAM, SQS, Lambda) that grant access to external principals. The unused access analyser generates periodic reports of IAM roles and users that have granted permissions not exercised in the review period – the raw material for quarterly access reviews:

# List unused access findings (roles with permissions not exercised in 90 days)
aws accessanalyzer list-findings \
  --analyzer-arn arn:aws:access-analyzer:eu-central-1:ACCOUNT:analyzer/unused-access \
  --filter '{"status": {"eq": ["ACTIVE"]}, "findingType": {"eq": ["UnusedPermission"]}}' \
  --query 'findings[].{Resource:resource,Principal:principal,LastAccess:updatedAt}' \
  --output table

Encryption and Data Protection

What NIS2/KRITIS require: Cryptography and encryption policies (Art. 21(2)(h)). For KRITIS, the BSI TR-02102 technical guidelines specify approved algorithms and key lengths. For personal data, GDPR Article 32 adds an encryption obligation.

All data at rest in a KRITIS environment must be encrypted with customer-managed KMS keys (CMKs), not AWS-managed keys. This distinction matters: with CMKs, you control the key policy, you can restrict which IAM principals can use the key, and you have audit visibility into every encryption/decryption operation via CloudTrail. With AWS-managed keys, you do not.

resource "aws_kms_key" "kritis_data" {
  description             = "KRITIS data encryption key - production"
  key_usage               = "ENCRYPT_DECRYPT"
  customer_master_key_spec = "SYMMETRIC_DEFAULT"
  enable_key_rotation     = true  # Annual automatic rotation

  deletion_window_in_days = 30  # Maximum protection against accidental deletion

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "EnableIAMUserPermissions"
        Effect = "Allow"
        Principal = {
          AWS = "arn:aws:iam::${var.account_id}:root"
        }
        Action   = "kms:*"
        Resource = "*"
      },
      {
        Sid    = "AllowKRITISApplicationUse"
        Effect = "Allow"
        Principal = {
          AWS = var.application_role_arns
        }
        Action = [
          "kms:Decrypt",
          "kms:GenerateDataKey",
          "kms:DescribeKey"
        ]
        Resource = "*"
      },
      {
        Sid    = "DenyKeyDeletionWithoutMFA"
        Effect = "Deny"
        Principal = {
          AWS = "*"
        }
        Action = [
          "kms:ScheduleKeyDeletion",
          "kms:DisableKey"
        ]
        Resource = "*"
        Condition = {
          BoolIfExists = {
            "aws:MultiFactorAuthPresent" = "false"
          }
        }
      }
    ]
  })
}

For KRITIS operators with hardware key control requirements (some energy and finance sector regulators mandate HSM-backed keys), use AWS CloudHSM with the EXTERNAL_KEY_STORE (XKS) feature. This keeps key material in an HSM you control, while retaining native AWS KMS integration. The latency penalty is approximately 3–5ms per crypto operation – evaluate this against your application performance requirements before committing.

Data in transit: enforce TLS 1.2 minimum, TLS 1.3 preferred, across all internal and external communication paths. AWS Certificate Manager manages certificates. Use an SCP to deny the creation of HTTP listeners on load balancers:

{
  "Sid": "DenyHTTPLoadBalancerListeners",
  "Effect": "Deny",
  "Action": "elasticloadbalancing:CreateListener",
  "Resource": "*",
  "Condition": {
    "StringEquals": {
      "elasticloadbalancing:Protocol": "HTTP"
    }
  }
}

Amazon Macie runs continuous classification jobs against your S3 buckets, identifying objects that contain PII, PHI, financial data, or credentials. For KRITIS-scoped S3 buckets, run daily Macie jobs and pipe findings to Security Hub. Any Macie finding indicating sensitive data in an unencrypted or public bucket should trigger an automated remediation via EventBridge and Lambda – the regulatory exposure from unencrypted personal data is compounded by GDPR if the data relates to individuals.

Vulnerability Management and Patching

What NIS2/KRITIS require: Vulnerability handling and disclosure policies (Art. 21(2)(e)). In practice: you need a continuous vulnerability scan, a documented process for prioritising and remediating findings, and evidence of timely patching.

Amazon Inspector v2 provides continuous vulnerability scanning for EC2 instances, ECR container images, and Lambda functions – no agent required for EC2 beyond the SSM agent. Inspector uses both CVE databases and a proprietary reachability analysis to produce an “Inspector score” that combines CVSS base score with environment-specific factors (internet exposure, presence of known exploit code).

The EPSS (Exploit Prediction Scoring System) integration in Inspector v2 is particularly useful for KRITIS prioritisation: it gives the probability of exploitation in the wild within 30 days. Prioritise vulnerabilities with EPSS > 0.1 (10%) regardless of CVSS score – CVSS measures theoretical severity, EPSS measures actual attacker interest.

# List CRITICAL findings across all accounts with EPSS > 0.1
aws inspector2 list-findings \
  --filter-criteria '{
    "findingStatus":[{"comparison":"EQUALS","value":"ACTIVE"}],
    "severity":[{"comparison":"EQUALS","value":"CRITICAL"}],
    "findingType":[{"comparison":"EQUALS","value":"PACKAGE_VULNERABILITY"}]
  }' \
  --query 'findings[?epss.score>`0.1`].{
    Resource:resources[0].id,
    CVE:packageVulnerabilityDetails.vulnerabilityId,
    CVSS:packageVulnerabilityDetails.cvss[0].baseScore,
    EPSS:epss.score,
    Title:title
  }' \
  --output table

For patching, AWS Systems Manager Patch Manager is the operational layer. Define patch baselines that specify: which packages require patching, the severity threshold (Critical and Important for KRITIS, not just Critical), and the maximum allowed time between patch availability and application. For KRITIS environments, I configure a 72-hour maximum for critical patches on internet-exposed systems, 14 days for all other critical patches.

resource "aws_ssm_patch_baseline" "kritis_linux" {
  name             = "kritis-rhel8-baseline"
  operating_system = "REDHAT_ENTERPRISE_LINUX"
  description      = "KRITIS patch baseline - 72h critical, 14d important"

  approval_rule {
    approve_after_days  = 3  # 72 hours
    enable_non_security = false

    patch_filter {
      key    = "CLASSIFICATION"
      values = ["Security"]
    }
    patch_filter {
      key    = "SEVERITY"
      values = ["Critical"]
    }
  }

  approval_rule {
    approve_after_days  = 14
    enable_non_security = false

    patch_filter {
      key    = "CLASSIFICATION"
      values = ["Security"]
    }
    patch_filter {
      key    = "SEVERITY"
      values = ["Important"]
    }
  }

  rejected_patches = []
  rejected_patches_action = "BLOCK"
}

Network Security and Segmentation

What NIS2/KRITIS require: Network security measures (Art. 21(2)(h)). The BSI IT-Grundschutz NET.1.1 building block specifies network architecture requirements including segmentation, monitoring, and filtering.

The architecture I implement for KRITIS environments uses a hub-and-spoke VPC model:

  • Inspection VPC: Centralised egress and east-west inspection via AWS Network Firewall. All traffic leaving any spoke VPC, and all cross-VPC traffic, passes through the inspection VPC. The Network Firewall uses Suricata-compatible rule groups – you can import commercial threat intelligence feeds directly.
  • DMZ VPC: Public-facing workloads only. Contains the load balancers, WAF, and CloudFront distributions. No direct database access from this VPC.
  • Application VPC(s): No internet route. All outbound AWS API calls via VPC interface endpoints (PrivateLink), eliminating internet egress for control plane traffic.
  • Data VPC: No route to the internet or to the application VPC except via specific, stateful security group rules. Contains all persistent data stores.

The critical Network Firewall configuration for KRITIS environments enforces known-bad domain blocking and anomalous protocol detection:

resource "aws_networkfirewall_rule_group" "kritis_domain_denylist" {
  capacity = 1000
  name     = "kritis-domain-denylist"
  type     = "STATEFUL"

  rule_group {
    rules_source {
      rules_source_list {
        generated_rules_type = "DENYLIST"
        target_types         = ["HTTP_HOST", "TLS_SNI"]
        targets = [
          ".tor2web.org",
          ".onion",
          # Import threat intel feed domains here
        ]
      }
    }

    stateful_rule_options {
      rule_order = "STRICT_ORDER"
    }
  }
}

For the data plane, VPC Flow Logs must be enabled on every VPC, capturing all traffic (not just rejected traffic). Store logs in S3 with Glacier lifecycle transitions, and make them queryable via Athena for incident investigation. BSI auditors will expect network traffic visibility during incident post-mortems.

Logging, Monitoring, and Audit Trails

What NIS2/KRITIS require: Audit trails that support incident investigation and compliance verification. The BSI IT-Grundschutz DER.2.1 (Incident management) building block requires event logs that cannot be manipulated by any account under investigation.

The logging architecture for tamper-evident audit trails:

CloudTrail must be configured as an org-wide trail with:

  • Log file validation enabled (SHA-256 hash chaining – detects any modification, deletion, or insertion of log files)
  • All management events, data events for S3 and Lambda, and CloudTrail Insights for anomalous API activity
  • Logs delivered to an S3 bucket in the dedicated Security/Audit account (member accounts have no write permission to this bucket)
  • S3 Object Lock on the destination bucket in compliance mode with a 7-year retention (required for KRITIS audit evidence)
# Enable CloudTrail Insights for anomaly detection on the org trail
aws cloudtrail put-insight-selectors \
  --trail-name org-trail-kritis \
  --insight-selectors '[
    {"InsightType": "ApiCallRateInsight"},
    {"InsightType": "ApiErrorRateInsight"}
  ]'

# Verify log file integrity for a specific time range
aws cloudtrail validate-logs \
  --trail-arn arn:aws:cloudtrail:eu-central-1:SECURITY_ACCOUNT:trail/org-trail-kritis \
  --start-time 2026-05-01T00:00:00Z \
  --end-time 2026-05-17T00:00:00Z \
  --verbose

S3 Object Lock is the critical tamper-proofing control. Once an object is locked in compliance mode, not even the AWS root account can delete or overwrite it before the retention period expires. This satisfies the KRITIS requirement that audit evidence cannot be manipulated by the entity being audited.

For real-time monitoring, Security Hub aggregates all findings and can forward them to your SIEM (Splunk, Microsoft Sentinel, IBM QRadar) via Kinesis Firehose. For KRITIS environments without an existing SIEM, you can build adequate monitoring using CloudWatch Logs Insights for ad-hoc queries and CloudWatch Metric Filters + Alarms for real-time alerting on specific conditions (console logins without MFA, root account usage, security group changes, etc.).

Physical Security (KRITIS-Specific)

KRITIS extends into physical security for on-premises systems and hybrid deployments. For pure-cloud KRITIS deployments, AWS’s physical security controls – documented in their ISO 27001 certification and C5 Testat – cover the data centre layer. You inherit these controls and document them as part of the shared responsibility model.

For hybrid environments where KRITIS-scoped systems connect to AWS, physical security of on-premises systems (network equipment connecting to AWS Direct Connect, HSMs in colocation facilities) remains the operator’s responsibility. Direct Connect is preferred over VPN for KRITIS-critical connections – it provides dedicated bandwidth, predictable latency, and does not traverse the public internet.


AWS Architecture for NIS2/KRITIS Compliance

The diagram below shows the full seven-layer reference architecture. Each layer maps to specific NIS2 Article 21 obligations and KRITIS § 8a control requirements.

The architecture flows top-to-bottom through the security layers:

  1. Perimeter (L1): All inbound traffic passes through CloudFront (TLS termination), AWS WAF (application-layer filtering), and Shield Advanced (DDoS absorption). Route 53 DNS Firewall blocks malicious domain resolution.
  2. Network (L2): Inside the perimeter, Network Firewall applies stateful deep-packet inspection and east-west controls. A strict subnet segmentation model separates public, application, and data tiers. VPC endpoints eliminate internet egress for AWS API calls. VPC Flow Logs capture all ENI traffic.
  3. Identity (L3): SCPs enforce hard guardrails at the Organizations level. Identity Center provides centralised, MFA-enforced human access. IAM Access Analyzer continuously detects over-privileged policies. KMS with CMKs controls all encryption operations.
  4. Detection (L4): GuardDuty, Security Hub, Inspector, Config, Macie, and SSM Patch Manager run continuously across all accounts. Security Hub aggregates findings centrally.
  5. Logging (L5): CloudTrail org-wide trail with log file validation feeds into Object Lock-protected S3 storage. Audit Manager collects evidence. AWS Artifact provides AWS’s compliance documentation (C5 Testat, ISO 27001, SOC 2).
  6. Response (L6): The NIS2 24-hour reporting workflow – GuardDuty → Security Hub → EventBridge → Step Functions → SNS – automates the first response steps and produces a notification-ready incident record within minutes.
  7. Business Continuity (L7): AWS Backup with cross-region copies, Elastic Disaster Recovery, and supply chain controls (CodeArtifact, ECR scanning, SBOM generation).

The NIS2 24-Hour Incident Notification Workflow

Article 23 NIS2 is one of the most operationally demanding provisions. Within 24 hours of becoming aware of a significant incident, you must submit an early warning to the BSI. “Becoming aware” is not defined as “concluding your investigation” – it means the moment you identify that an incident has occurred. In practice, this means your detection-to-notification pipeline must work automatically and must not depend on an analyst being available.

The automated workflow I implement:

GuardDuty (T+0: finding detected)
    ↓  [all HIGH/CRITICAL findings]
Security Hub (T+1min: severity enriched, deduplicated)
    ↓  [ASFF event to EventBridge]
EventBridge Rule (T+2min: pattern matched on severity + KRITIS account tag)
    ↓  [state machine input]
Step Functions (T+2-15min: IR state machine)
    ├── Lambda: Triage (classify finding type, map to KRITIS asset)
    ├── Lambda: Containment (isolate EC2, revoke temporary credentials)
    ├── Lambda: Evidence (EBS snapshot, CloudTrail export, VPC flow log preservation)
    └── Lambda: Notification assembly (populate BSI report template)

SNS (T+15min: alert to CSIRT on-call + Jira ticket created)

Human analyst: review notification draft, approve BSI submission

BSI MELDEPFLICHT portal: submit (T < 24h from detection)

The EventBridge rule pattern that triggers the KRITIS notification workflow:

{
  "source": ["aws.guardduty", "aws.securityhub"],
  "detail-type": [
    "GuardDuty Finding",
    "Security Hub Findings - Imported"
  ],
  "detail": {
    "findings": {
      "Severity": {
        "Label": ["HIGH", "CRITICAL"]
      },
      "Resources": {
        "Tags": {
          "ComplianceScope": ["KRITIS", "BOTH"]
        }
      }
    }
  }
}

The tag condition is critical: it ensures the notification workflow fires specifically for KRITIS-tagged resources, not for every HIGH/CRITICAL finding across all accounts. Without this scope filter, non-KRITIS workloads flood the notification pipeline and cause alert fatigue that defeats the purpose.

The notification assembly Lambda generates a pre-populated BSI incident notification template:

import boto3
import json
from datetime import datetime, timezone

def handler(event, context):
    finding = event['finding']
    
    # Map GuardDuty finding type to BSI incident category
    incident_category_map = {
        "UnauthorizedAccess": "unbefugter Zugriff",
        "CryptoCurrency": "Cryptomining / Ressourcenmissbrauch",
        "Backdoor": "Backdoor / persistenter Zugriff",
        "Trojan": "Schadprogramm",
        "Recon": "Aufklärung / Scanning",
        "Policy": "Richtlinienverletzung",
    }
    
    finding_type_prefix = finding['Type'].split(':')[0]
    bsi_category = incident_category_map.get(finding_type_prefix, "Sonstiges")
    
    bsi_report = {
        "meldezeitpunkt": datetime.now(timezone.utc).isoformat(),
        "ersterkennungszeitpunkt": finding['CreatedAt'],
        "betroffene_anlage": {
            "bezeichnung": finding['Resources'][0].get('Tags', {}).get('Name', 'unbekannt'),
            "kritis_sektor": finding['Resources'][0].get('Tags', {}).get('KRITISSektor', 'unbekannt'),
            "aws_account": finding['AccountId'],
            "aws_region": finding['Region'],
        },
        "vorfallkategorie": bsi_category,
        "schweregrad": finding['Severity']['Label'],
        "beschreibung": finding['Description'],
        "betroffene_dienste": "wird ermittelt",
        "massnahmen_ergriffen": "Isolation initiiert via AWS Step Functions IR-Workflow",
        "meldepflichtig_nach": "§ 8b BSIG / NIS2 Art. 23",
    }
    
    # Store report in S3 and send to SNS
    s3 = boto3.client('s3')
    s3.put_object(
        Bucket='kritis-incident-reports',
        Key=f"bsi-report-draft-{finding['Id']}.json",
        Body=json.dumps(bsi_report, ensure_ascii=False, indent=2),
        ContentType='application/json'
    )
    
    sns = boto3.client('sns')
    sns.publish(
        TopicArn='arn:aws:sns:eu-central-1:ACCOUNT:kritis-csirt-alerts',
        Subject=f"[KRITIS MELDEPFLICHT] {bsi_category} - {finding['Severity']['Label']} - {finding['AccountId']}",
        Message=json.dumps(bsi_report, ensure_ascii=False, indent=2)
    )
    
    return {"status": "notification_dispatched", "report_id": finding['Id']}

The human analyst receives the pre-populated BSI report, verifies the details against the incident investigation, and submits via the BSI’s MELDEPFLICHT portal or the ENISA reporting system. The automated workflow ensures the 24-hour deadline is structurally reachable – it does not guarantee it if your CSIRT is unresponsive, but it eliminates the scenario where a finding sat in a queue unnoticed.


AWS Audit Manager: Building a Custom NIS2 Framework

AWS Audit Manager lets you create custom assessment frameworks that map NIS2 Article 21 obligations to specific AWS control evidence. This is the operational backbone of your BSI compliance submission.

The framework structure maps NIS2 control domains to AWS evidence sources:

# Boto3: create a custom NIS2 control set in Audit Manager
import boto3

auditmanager = boto3.client('auditmanager', region_name='eu-central-1')

# Create a control for NIS2 Art. 21(2)(i) - MFA enforcement
control = auditmanager.create_control(
    name='NIS2-Art21-2i-MFA-Enforcement',
    description='Verify MFA is enforced for all IAM users and Identity Center users',
    testingInformation='Check Security Hub FSBP.IAM.6 and CIS 1.10 findings. Verify IAM Identity Center MFA settings.',
    actionPlanTitle='Enable MFA for non-compliant users',
    actionPlanInstructions='Enforce FIDO2 MFA via Identity Center. Apply SCP to deny console access without MFA.',
    controlMappingSources=[
        {
            'sourceName': 'SecurityHub-MFA-Check',
            'sourceDescription': 'Security Hub check for MFA on IAM users',
            'sourceSetUpOption': 'System_Controls_Mapping',
            'sourceType': 'AWS_Security_Hub',
            'sourceKeyword': {
                'keywordInputType': 'SELECT_FROM_LIST',
                'keywordValue': 'arn:aws:securityhub:::controls/aws-foundational-security-best-practices/v/1.0.0/IAM.6'
            },
            'troubleshootingText': 'Navigate to Security Hub → Standards → FSBP → IAM.6'
        },
        {
            'sourceName': 'CloudTrail-Console-SignIn-No-MFA',
            'sourceDescription': 'CloudTrail events for console sign-ins without MFA',
            'sourceSetUpOption': 'Procedural_Controls_Mapping',
            'sourceType': 'MANUAL',
            'troubleshootingText': (
                'Query CloudTrail: filter ConsoleLogin events where '
                'additionalEventData.MFAUsed = No'
            )
        }
    ]
)

Each NIS2 Article 21 sub-clause becomes a control set in the framework. Audit Manager collects evidence automatically from Config rules, Security Hub findings, and CloudTrail events. Manual evidence (third-party audit reports, vendor security questionnaires, penetration test results) is uploaded directly. The result is an auditor-ready assessment report that maps every control to its evidence – exactly what a BSI audit engagement requires.


AWS Artifact: Leveraging AWS’s Compliance Documentation

AWS holds numerous third-party certifications that cover the infrastructure layer. For KRITIS compliance, the most relevant documents available from AWS Artifact are:

  • BSI C5 Testat (Cloud Computing Compliance Criteria Catalogue): Covers eu-central-1 (Frankfurt) and eu-west-1 (Ireland). This is the BSI’s own cloud security standard, and AWS holding this testat means auditors can rely on AWS’s controls for the infrastructure layer without re-auditing the data centre.
  • ISO 27001 Certificate: Covers all commercial AWS regions. Required baseline for most KRITIS auditors.
  • SOC 2 Type II Report: Documents AWS’s security, availability, and confidentiality controls with semi-annual independent auditor verification.
  • ISO 27017 (Cloud-specific security controls) and ISO 27018 (PII protection in cloud) certificates.
# Download AWS Artifact agreements programmatically
aws artifact list-reports \
  --query 'reports[?category==`Certifications`].{Name:name,Period:period}' \
  --output table

# Accept the NDA for a specific report and get download URL
aws artifact get-report-url \
  --report-id <report-id> \
  --report-version <version>

The key message for auditors: AWS’s C5 Testat covers the infrastructure layer. Your organisation’s controls must cover the application and configuration layer. The two together constitute the complete compliance picture under shared responsibility.


Practical Implementation Roadmap

Starting a NIS2/KRITIS compliance programme on AWS from scratch is daunting. The following phased roadmap reflects what I have learned deploying this in practice – what you actually need to do in what order to avoid compliance gaps and rework.

Phase 0: Scoping and Inventory (Week 1–2)

Before you configure a single AWS service, you need to know what you are protecting:

  1. Determine whether you qualify as an essential entity or important entity under NIS2. If you are in Germany, also check whether you exceed the BSI-KritisV sector thresholds for KRITIS designation.
  2. Register with the BSI via the KRITIS portal if you meet KRITIS thresholds. Failure to register is itself a violation.
  3. Identify all AWS accounts, regions, and services in scope. Tag all KRITIS-critical resources with ComplianceScope: KRITIS.
  4. Map your data flows – which data enters your KRITIS-scoped systems, where it is stored, and which third parties have access.

Phase 1: Quick Wins (Days 1–30)

These controls have low implementation effort and high compliance impact. They also satisfy the most scrutinised controls in BSI audits:

ControlAWS ServiceTime to Implement
Enable GuardDuty across all accountsAWS Organizations + GuardDuty2 hours
Enable Security Hub + CIS/FSBP standardsSecurity Hub2 hours
Enable CloudTrail org-wide trail with validationCloudTrail4 hours
Enable S3 Object Lock on log bucketsS31 hour
Deploy MFA enforcement SCPAWS Organizations2 hours
Enable AWS Config with conformance packsConfig4 hours
Enable Inspector v2 across all accountsInspector1 hour
Enable VPC Flow Logs on all VPCsVPC2 hours
Enable Macie on KRITIS S3 bucketsMacie2 hours
Rotate all long-lived IAM access keysIAM4–8 hours
Enable AWS Backup for critical resourcesAWS Backup4 hours
Download C5 Testat from AWS ArtifactAWS Artifact30 minutes

This 30-day sprint addresses the most commonly cited deficiencies in BSI KRITIS audits and gives you an initial Security Hub compliance score to baseline against.

Phase 2: Architecture Hardening (Days 31–60)

  1. Network segmentation: Implement the hub-and-spoke VPC model with AWS Network Firewall in the inspection VPC. Migrate public-facing workloads to the DMZ VPC. Configure VPC endpoints for all AWS services used by application workloads.
  2. Identity hardening: Deploy IAM Identity Center with corporate IdP integration. Migrate all human IAM users to Identity Center. Enforce FIDO2 MFA. Delete all IAM users with console access. Run the IAM Access Analyzer unused access report and remediate.
  3. Encryption uplift: Identify all resources using AWS-managed keys and migrate to CMKs. Enable automatic key rotation on all CMKs. Implement KMS key policies with data classification separation.
  4. Patch management: Deploy SSM Patch Manager with KRITIS patch baselines. Enrol all EC2 instances in maintenance windows. Verify SSM agent coverage is 100% on KRITIS-scoped instances.
  5. IR automation: Deploy the EventBridge → Step Functions → SNS incident notification pipeline. Test with a synthetic GuardDuty finding (use GuardDuty’s sample findings feature). Verify the BSI notification draft is generated correctly.

Phase 3: Compliance Operationalisation (Days 61–90)

  1. Audit Manager framework: Create the custom NIS2 assessment framework. Assign it to all KRITIS-scoped accounts. Review the initial evidence collection and remediate gaps.
  2. Vulnerability management process: Define CVSS/EPSS thresholds and SLA targets. Integrate Inspector findings with your ticketing system. Run the first patch compliance report and remediate all CRITICAL findings.
  3. Supply chain controls: Implement CodeArtifact proxies for all package managers. Enable ECR enhanced scanning. Define and implement an SBOM generation process for KRITIS-critical container images.
  4. DR testing: Execute a DR drill – recover a KRITIS-scoped RDS instance from cross-region backup to eu-west-1. Document RTO achieved vs. RTO target. Store drill evidence in Audit Manager.
  5. Penetration test: Commission an external penetration test of KRITIS-scoped systems. BSI auditors expect an annual penetration test as evidence of proactive risk management. The test results – including remediated findings – become Audit Manager evidence.
  6. Documentation package: Prepare the BSI audit submission: security concept, risk register, technical measures list mapped to BSI IT-Grundschutz building blocks, ISMS documentation, and the Audit Manager assessment report.

Ongoing: Compliance-as-Operations

The steady state is not a project – it is a continuous operational programme:

  • Weekly: Review Security Hub compliance score trends. Triage new Inspector findings.
  • Monthly: Run IAM Access Analyzer unused access report. Review CloudTrail Insights anomalies.
  • Quarterly: Access review for all KRITIS-scoped IAM roles. Key policy review. Supplier security questionnaire follow-up.
  • Annually: External penetration test. BSI KRITIS evidence submission (every 2 years, alternating years for internal audit).
  • Continuously: GuardDuty monitoring, EventBridge incident workflow, Patch Manager compliance, Config rule evaluation.

What AWS Does Not Cover

Being precise about the gaps in the AWS-native approach saves you the embarrassment of discovering them in a BSI audit:

SOC processes: AWS services generate telemetry and findings. They do not analyse them. You need human analysts who understand the alerts, can distinguish true positives from false positives, and can conduct incident investigations. If you do not have an internal SOC capability, you need a MSSP – and under NIS2, your MSSP relationship is itself a supply chain security obligation (Art. 21(2)(d)) requiring formal security assessment.

Penetration testing: AWS Config rules and Security Hub findings do not substitute for penetration testing. Config rules check configuration; they do not test whether a determined attacker can chain multiple findings into a breach. Annual penetration tests of KRITIS-scoped systems are a BSI expectation.

Physical security for hybrid environments: If you have on-premises systems that feed into AWS (Direct Connect, VPN, on-premises processing that feeds S3), those physical systems are outside the shared responsibility model. Their physical and logical security is entirely your obligation.

Employee security training: NIS2 Art. 21(2)(g) requires cyber hygiene training for all personnel handling KRITIS-relevant systems. AWS has no service for this. This is a human process.

ISMS documentation: NIS2 requires documented security policies, risk management processes, and governance structures. AWS services generate evidence that you can point to. They do not write your ISMS for you.


Conclusion

KRITIS and NIS2 compliance on AWS is tractable, but it is not a checkbox exercise. The regulatory frameworks are specific enough that vague architectural statements – “we use encryption” or “we have monitoring” – will not survive a BSI audit. Auditors want to see the KMS key policy, the CloudTrail log validation output, the Patch Manager compliance dashboard showing 100% coverage, and the tested DR recovery time.

The AWS service landscape maps cleanly onto the NIS2 Article 21 control domains, with a few important caveats: you need CMKs (not AWS-managed keys) for encryption, you need Object Lock (not just versioning) for tamper-proof logs, and you need an org-wide CloudTrail (not account-level trails) for comprehensive audit coverage. These distinctions are not obvious from the service documentation but they are the ones that matter in an audit.

The 24-hour incident notification requirement in Art. 23 is the operational forcing function that makes the entire detection-to-response pipeline non-optional. If you cannot reliably get from “GuardDuty finding detected” to “BSI notification submitted” in under 24 hours without depending on an analyst being awake and available, you are non-compliant. Building the EventBridge → Step Functions notification workflow is not optional for KRITIS operators – it is the minimum automation needed to make the legal obligation structurally achievable.

Finally: if you are not registered with the BSI and you meet the KRITIS thresholds, fix that first. Unregistered KRITIS operators are easy to identify (sector-specific threshold checks are not secret) and face the same penalties as registered operators who are non-compliant with technical measures – plus additional penalties for the failure to register. The registration obligation is independent of and prior to any technical implementation work.

References