Skip to content

Compliance Exemptions

Some resources will fail a compliance check for reasons you have already accepted: a test workload inside the boundary that does not need cross-region backup, a legacy service whose risk is formally documented. Left alone, those failures accumulate and the scan stops being a signal -- everyone learns to ignore the red.

The Stratus compliance mod handles this with resource tags. Tag a resource with the check it should be excused from, and the scan reports it as skipped rather than failing, carrying your justification alongside the result. The exemption lives on the resource, in your cloud, in the same place as the thing being exempted.

Quick Summary

  • Three tags do the work: compliance_exempt holds the check IDs, compliance_exemption_reason holds the justification, compliance_exemption_expiry holds the date it lapses.
  • The tag value is colon-separated with no spaces. KSI-A:KSI-B works; KSI-A: KSI-B and KSI-A,KSI-B are silently ignored.
  • Tag with a KSI ID to excuse every check under that indicator, or a check ID to excuse exactly one.
  • 172 of 226 checks honor the tag. The rest grade account-level settings or untaggable objects -- see Checks that cannot be exempted.
  • An expiry date in the past turns the exemption back into a failure labelled EXPIRED. A malformed date means the exemption never expires, so the format matters.
  • Skipped resources count as neither passing nor failing, so an exemption narrows what the check measures. Pair every one with a Vulnerability Deviation.

An exemption is a risk decision, not a cleanup step

Tagging a resource removes it from the evidence an assessor reviews. That is a legitimate thing to do with a documented, accepted, time-bounded risk, and an audit finding waiting to happen without one. Record the accepted risk as a Vulnerability Deviation first, then reference it in the exemption reason. Anyone with tag-write access in your cloud can suppress a compliance failure, so treat tag permissions accordingly.


The three tags

Tag key Required Value Effect
compliance_exempt Yes Colon-separated KSI and check IDs, for example KSI-RPL-ABO.11 or KSI-CNA-IBP:KSI-SVC-ACM.10 Matching checks report the resource as skip instead of alarm
compliance_exemption_reason Strongly recommended Free text. Reference the governing Vulnerability Deviation Appended to the check's result text, so the justification travels with the evidence
compliance_exemption_expiry Strongly recommended A date in YYYY-MM-DD format After this date the resource fails again, labelled EXPIRED exemption

The reason and expiry tags do nothing on their own. They are only read for resources that already match on compliance_exempt.

These are the default tag keys. They are configurable in the mod, so if your deployment overrides them, use your deployment's keys instead.


Getting the format exactly right

The tag value is split on the colon character with no cleanup: no whitespace trimming, no alternative separators. The value is matched case-insensitively, but everything else is literal. These are the behaviors that matter, each one verified against the scan logic:

Tag value Result Why
KSI-RPL-ABO.11 Skipped Exact check ID
KSI-RPL-ABO Skipped Indicator ID excuses every check under it
ksi-rpl-abo.11 Skipped Value matching is case-insensitive
KSI-CNA-IBP:KSI-RPL-ABO.11 Skipped Multiple IDs, colon-separated, no spaces
KSI-CNA-IBP: KSI-RPL-ABO.11 Ignored The space becomes part of the second ID, so it never matches
KSI-CNA-IBP,KSI-RPL-ABO.11 Ignored Comma is not a separator. The whole value is read as one ID
KSI-RPL-ABO.11 Ignored Trailing whitespace is part of the ID
KSI-RPL-ABO.1 Ignored Partial IDs do not match. It is exact or nothing

Every one of these failures is silent

A malformed tag produces no warning, no error, and no log line. The resource simply keeps failing exactly as it did before, and the check output says nothing about a tag being present. The only way to know your tag worked is to confirm the resource now reports as skipped. Always verify after tagging.

The tag key is case-sensitive even though the value is not. Compliance_Exempt and COMPLIANCE_EXEMPT do not work -- only compliance_exempt. This bites hardest in the Azure portal, which preserves whatever capitalization you type.

Expiry dates

The expiry tag is matched against a strict YYYY-MM-DD pattern. Anything else fails the pattern and is treated as no expiry at all, which means the exemption is permanent:

Expiry value Result
2027-01-01 (future) Skipped, exemption active
2026-01-01 (past) Fails, labelled EXPIRED exemption with the date
Today's date Still skipped. The exemption lapses the following day
01/01/2026 Never expires -- pattern does not match
Jan 1 2026 Never expires -- pattern does not match
2026-01-01T00:00:00Z Never expires -- the time portion breaks the pattern

Use plain YYYY-MM-DD with no time component. An expired exemption is designed to be loud: it fails the check and says why, which is what forces the renew-or-remediate conversation instead of letting the exemption quietly outlive its justification.


Choosing what to put in the tag

Indicator ID or check ID

Tagging KSI-RPL-ABO excuses the resource from every check under that indicator. Tagging KSI-RPL-ABO.11 excuses it from that one check only.

Prefer the specific check ID. An indicator-level exemption is a standing instruction to ignore a whole class of finding on that resource, including checks added to the indicator later, which you have not reviewed and did not intend to excuse.

One tag can affect several indicators

Twenty-nine checks are shared across multiple indicators -- the same test evidences more than one KSI. Excusing a resource from one of them excuses it from all of them, because there is only one underlying test.

The widest example is the Azure Key Vault secret expiry check, which evidences seven indicators at once: KSI-IAM-AAM.20, KSI-IAM-AAM.21, KSI-SVC-ACM.21, KSI-SVC-ACM.22, KSI-SVC-ACM.23, KSI-SVC-ASM.20, and KSI-SVC-SIN.20. A single tag on one secret moves the needle on all seven. Check the reference table for what a check ID actually covers before you use it.


Which resource to tag

Usually the answer is obvious: the resource named in the failing result is the one to tag. In eight cases it is not, because the check grades rows that cannot carry tags and keys the exemption off the parent instead.

Tag the parent, not the graded row

The check grades Tag this instead Affected checks
IAM access keys The IAM user that owns the key KSI-IAM-APM.11, KSI-IAM-SNU.11
IAM credential report rows The IAM user KSI-IAM-APM.14
VPC endpoint coverage The VPC KSI-CNA-MAT.13
Security group rules The security group KSI-CNA-DFP.10, KSI-CNA-DFP.11, KSI-CNA-DFP.13, KSI-CNA-IBP.11, KSI-CNA-RNT.10, KSI-CNA-RNT.11

Note what this means for security groups: you cannot exempt one offending rule. The exemption applies to the whole group, including rules added later. If that is too broad, the answer is to split the group, not to widen the exemption.

Reference: every exemptable check by resource type

Find the resource type you are tagging, and the checks that tag can excuse.

AWS -- 117 checks
Tag this resource Steampipe table Checks it can exempt
Application Load Balancer aws_ec2_application_load_balancer KSI-CNA-IBP.15, KSI-CNA-OFA.12, KSI-CNA-RVP.11, KSI-MLA-LET.13
Auto Scaling group aws_ec2_autoscaling_group KSI-CMT-RMV.11, KSI-CNA-OFA.10, KSI-SVC-PRR.10
AWS Config rule aws_config_rule KSI-CNA-ULN.10, KSI-CNA-ULN.12, KSI-MLA-EVC.11, KSI-MLA-EVC.12, KSI-MLA-OSM.11, KSI-SVC-PRR.12
Backup plan aws_backup_plan KSI-RPL-ABO.10, KSI-RPL-ARP.10
Backup recovery point aws_backup_recovery_point KSI-RPL-ABO.15
Backup vault aws_backup_vault KSI-RPL-ABO.11, KSI-RPL-ARP.11, KSI-RPL-RRO.10
CloudFormation stack aws_cloudformation_stack KSI-SVC-PRR.11
CloudTrail trail aws_cloudtrail_trail KSI-CMT-LMC.10, KSI-CMT-VTD.10, KSI-MLA-LET.10, KSI-SVC-VRI.10
CloudWatch alarm aws_cloudwatch_alarm KSI-CNA-RVP.13, KSI-IAM-SUS.11
CloudWatch log group aws_cloudwatch_log_group KSI-INR-AAR.11, KSI-MLA-ALA.11, KSI-MLA-LET.11, KSI-MLA-RVL.10, KSI-MLA-RVL.11, KSI-MLA-RVL.12, KSI-SVC-SIN.13
CodeBuild project aws_codebuild_project KSI-CMT-RVP.11, KSI-CMT-VTD.11, KSI-PIY-RSD.10
CodePipeline pipeline aws_codepipeline_pipeline KSI-CMT-RVP.10, KSI-CMT-VTD.12, KSI-PIY-RSD.11, KSI-RPL-TRC.12
DynamoDB table aws_dynamodb_table KSI-RPL-ARP.12, KSI-RPL-RRO.12, KSI-RPL-TRC.11, KSI-SVC-ACM.14, KSI-SVC-RUD.11
EBS volume aws_ebs_volume KSI-SVC-SIN.10
EC2 instance aws_ec2_instance KSI-CMT-RMV.12, KSI-CNA-EIS.10, KSI-CNA-EIS.11, KSI-CNA-IBP.10, KSI-CNA-IBP.12, KSI-IAM-APM.13, KSI-PIY-GIV.11, KSI-SCR-MON.11, KSI-SVC-VRI.11
EC2 launch template aws_ec2_launch_template KSI-CMT-RMV.10
ECR repository aws_ecr_repository KSI-INR-RPI.11, KSI-PIY-RVD.10
ECS service aws_ecs_service KSI-CNA-OFA.14
GuardDuty detector aws_guardduty_detector KSI-CNA-RVP.12, KSI-IAM-SUS.10, KSI-INR-AAR.10, KSI-INR-RIR.10, KSI-INR-RIR.11, KSI-INR-RIR.12, KSI-INR-RPI.10
IAM instance profile aws_iam_instance_profile KSI-IAM-APM.12
IAM policy aws_iam_policy KSI-IAM-ELP.10
IAM role aws_iam_role KSI-IAM-APM.10, KSI-IAM-ELP.12, KSI-SVC-VRI.14
IAM user aws_iam_user KSI-IAM-AAM.12, KSI-IAM-APM.11, KSI-IAM-APM.14, KSI-IAM-ELP.11, KSI-IAM-JIT.10, KSI-IAM-SNU.10, KSI-IAM-SNU.11
KMS key aws_kms_key KSI-SVC-ACM.10, KSI-SVC-ASM.10, KSI-SVC-ASM.11
Lambda function aws_lambda_function KSI-IAM-ELP.13, KSI-PIY-RSD.12, KSI-SVC-VCM.10, KSI-SVC-VRI.13
Network ACL aws_vpc_network_acl KSI-CNA-DFP.12
RDS DB instance aws_rds_db_instance KSI-CNA-IBP.14, KSI-CNA-OFA.13, KSI-RPL-ABO.12, KSI-RPL-RRO.11, KSI-SCR-MON.10, KSI-SVC-SIN.12, KSI-SVC-VCM.11
Route table aws_vpc_route_table KSI-CNA-MAT.12
S3 bucket aws_s3_bucket KSI-CNA-IBP.13, KSI-MLA-ALA.10, KSI-PIY-GIV.12, KSI-RPL-ABO.13, KSI-RPL-ABO.14, KSI-RPL-TRC.10, KSI-SVC-RUD.10, KSI-SVC-SIN.11, KSI-SVC-VRI.12
Security group aws_vpc_security_group KSI-CNA-DFP.10, KSI-CNA-DFP.11, KSI-CNA-DFP.13, KSI-CNA-IBP.11, KSI-CNA-RNT.10, KSI-CNA-RNT.11
Subnet aws_vpc_subnet KSI-CNA-MAT.11
VPC aws_vpc KSI-CNA-MAT.10, KSI-CNA-MAT.13, KSI-CNA-RNT.12, KSI-MLA-LET.14
VPC peering connection aws_vpc_peering_connection KSI-CNA-MAT.14
WAFv2 web ACL aws_wafv2_web_acl KSI-CNA-RNT.13, KSI-CNA-RVP.10
Azure -- 55 checks
Tag this resource Steampipe table Checks it can exempt
AKS cluster azure_kubernetes_cluster KSI-CNA-EIS.21, KSI-CNA-IBP.24, KSI-SVC-VCM.20
App Service web app azure_app_service_web_app KSI-CNA-EIS.24
Application Gateway azure_application_gateway KSI-CNA-DFP.23, KSI-CNA-MAT.21, KSI-CNA-RVP.20, KSI-SVC-ACM.24
Container registry azure_container_registry KSI-CNA-EIS.22, KSI-CNA-EIS.23
Key vault azure_key_vault KSI-SVC-RUD.20
Key Vault key azure_key_vault_key KSI-SVC-ACM.20
Key Vault secret azure_key_vault_secret KSI-IAM-AAM.20, KSI-IAM-AAM.21, KSI-SVC-ACM.21, KSI-SVC-ACM.22, KSI-SVC-ACM.23, KSI-SVC-ASM.20, KSI-SVC-SIN.20
Log Analytics workspace azure_log_analytics_workspace KSI-MLA-ALA.20, KSI-MLA-LET.24, KSI-MLA-LET.25, KSI-MLA-OSM.20
MySQL server azure_mysql_server KSI-CNA-MAT.24
Network security group azure_network_security_group KSI-CNA-DFP.20, KSI-CNA-RNT.20
PostgreSQL server azure_postgresql_server KSI-CNA-MAT.23
Private endpoint azure_private_endpoint KSI-CNA-ULN.20
Recovery Services vault azure_recovery_services_vault KSI-CNA-OFA.20, KSI-RPL-ABO.20, KSI-RPL-ARP.20, KSI-RPL-TRC.20
Redis cache azure_redis_cache KSI-CNA-MAT.22
SQL database azure_sql_database KSI-CNA-IBP.21, KSI-RPL-RRO.20
SQL server azure_sql_server KSI-IAM-APM.21, KSI-MLA-LET.23, KSI-PIY-RVD.20, KSI-SVC-PRR.20
Storage account azure_storage_account KSI-CNA-IBP.22, KSI-CNA-IBP.23, KSI-CNA-MAT.20, KSI-IAM-AAM.22, KSI-IAM-SNU.20, KSI-RPL-RRO.21, KSI-SVC-ACM.25, KSI-SVC-VRI.20
Subscription azure_subscription KSI-IAM-SUS.20, KSI-INR-AAR.20
Virtual machine azure_compute_virtual_machine KSI-CMT-RMV.21, KSI-CNA-DFP.21, KSI-CNA-IBP.20, KSI-PIY-RSD.20
VM scale set azure_compute_virtual_machine_scale_set KSI-CMT-RMV.20, KSI-CNA-EIS.20

Checks that cannot be exempted

Fifty-four checks ignore the tag entirely. Tagging a resource for one of these does nothing at all -- it will not error, and the check will keep failing.

Category Why the tag cannot work Checks
Account and subscription settings The check grades the account or subscription itself rather than a resource, so there is nothing to tag AWS: KSI-CMT-LMC.11, KSI-CNA-IBP.16, KSI-IAM-AAM.10, KSI-IAM-AAM.11, KSI-IAM-SNU.12, KSI-MLA-EVC.10, KSI-MLA-LET.12, KSI-MLA-OSM.10, KSI-PIY-GIV.10, KSI-PIY-RES.10, KSI-SVC-EIS.10, KSI-SVC-EIS.11, KSI-SVC-EIS.12
Azure: KSI-CMT-LMC.20, KSI-CMT-LMC.21, KSI-CMT-RVP.20, KSI-IAM-APM.20, KSI-IAM-ELP.20, KSI-IAM-SUS.21, KSI-INR-AAR.21, KSI-INR-RIR.20, KSI-MLA-EVC.20, KSI-MLA-LET.21, KSI-MLA-LET.22, KSI-MLA-RVL.20, KSI-PIY-GIV.20, KSI-PIY-GIV.21, KSI-PIY-GIV.22, KSI-SCR-MON.20, KSI-SVC-EIS.20
Objects with no tag support Entra ID directory objects -- users, service principals, conditional access policies, PIM assignments -- carry no tags All 18 Entra ID checks: KSI-IAM-AAM.30, KSI-IAM-AAM.31, KSI-IAM-APM.30 through KSI-IAM-APM.34, KSI-IAM-ELP.30 through KSI-IAM-ELP.32, KSI-IAM-JIT.31, KSI-IAM-JIT.32, KSI-IAM-SNU.30 through KSI-IAM-SNU.32, KSI-IAM-SUS.30, KSI-MLA-ALA.30, KSI-MLA-ALA.31
The graded resource exposes no tags to the scan KSI-CNA-OFA.11 (ElastiCache replication groups), KSI-CNA-ULN.11 (SSM associations)
Not currently supported These checks do not honor the exemption tag today KSI-SVC-ACM.11, KSI-SVC-ACM.12, KSI-SVC-ACM.13, KSI-SVC-ASM.12 (Secrets Manager)

For these, an accepted risk has to be handled in GRC-ITSM rather than in the cloud: record the Vulnerability Deviation and let the check keep reporting its finding, with the deviation as the governing record.


Tagging a resource

The tag is an ordinary cloud resource tag. Apply it however you normally manage tags -- infrastructure as code is strongly preferred, because a tag applied by hand in a console is invisible to review and disappears on the next redeploy.

resource "aws_backup_vault" "test_workloads" {
  name = "test-workloads"

  tags = {
    compliance_exempt            = "KSI-RPL-ABO.11"
    compliance_exemption_reason  = "VD-1284: non-production test vault, encryption not required. Risk accepted by CISO."
    compliance_exemption_expiry  = "2027-02-01"
  }
}
resource "azurerm_storage_account" "test_data" {
  name                = "testdata"
  resource_group_name = azurerm_resource_group.test.name
  location            = azurerm_resource_group.test.location

  tags = {
    compliance_exempt           = "KSI-CNA-IBP.22"
    compliance_exemption_reason = "VD-1290: test fixture data, no federal information. Risk accepted."
    compliance_exemption_expiry = "2027-02-01"
  }
}
aws backup tag-resource \
  --resource-arn "arn:aws:backup:us-east-1:111122223333:backup-vault:test-workloads" \
  --tags '{
    "compliance_exempt": "KSI-RPL-ABO.11",
    "compliance_exemption_reason": "VD-1284: non-production test vault. Risk accepted by CISO.",
    "compliance_exemption_expiry": "2027-02-01"
  }'
az resource tag \
  --ids "/subscriptions/<sub>/resourceGroups/test/providers/Microsoft.Storage/storageAccounts/testdata" \
  --tags \
    compliance_exempt="KSI-CNA-IBP.22" \
    compliance_exemption_reason="VD-1290: test fixture data, no federal information." \
    compliance_exemption_expiry="2027-02-01" \
  --is-incremental

Use --is-incremental so you add these tags instead of replacing every tag on the resource.

Multiple checks on one resource go in a single compliance_exempt value, colon-separated:

compliance_exempt = "KSI-RPL-ABO.11:KSI-RPL-RRO.10"

Verify an exemption

Because every formatting mistake fails silently, confirm the result rather than assuming it.

  1. Re-run the scan. Wait for the next scheduled run or trigger one by hand.
  2. Check the resource's status. It should report as skip, not alarm. If it still fails, the tag did not match -- re-read Getting the format exactly right, and check the tag key's capitalization first.
  3. Confirm the reason came through. The result text should carry your justification. A skip with no reason means compliance_exemption_reason is missing or its key is misspelled.
  4. Confirm the counts moved. In GRC-ITSM, open the child validation ticket whose summary starts with that check ID and read the newest row on its Validations tab. Fail Count should drop by the resources you exempted, and Pass Count should not increase, because skipped resources count as neither. A Fail Count that renders blank means zero failures.

That last point is the one people misread. An exempted resource does not become evidence of compliance; it drops out of the measurement entirely. A check with ten resources where six pass and four are exempt reports six passing, not ten.


Keeping exemptions honest

An exemption tag is a control you have chosen not to enforce, so it needs the same lifecycle as any other accepted risk.

Practice Why
Always set an expiry An exemption with no expiry never comes back for review. Set the date to when the accepted risk is due for reassessment
Reference the deviation in the reason The reason text travels into the scan output, so the evidence carries its own justification and an assessor does not have to go hunting
Manage tags in code A tag in Terraform is reviewable, attributable, and survives redeployment. A tag added in a console is none of those things
Review expiring exemptions before they lapse An expired exemption fails loudly and can turn into an SLA-bound finding overnight. Renew or remediate on your schedule, not the scanner's
Re-examine indicator-level exemptions A KSI-XXX-YYY tag silently covers checks added to that indicator after you set it. Narrow to specific check IDs where you can
Watch the total A rising exemption count across the estate is a signal about the environment or the baseline, not a housekeeping detail. Report it alongside the KSI status

Every exemption is visible in the scan output, including its reason and expiry, so the set of exemptions is itself reviewable evidence. That is the point of putting them in tags rather than suppressing findings after the fact: the decision stays attached to the resource, and it shows up in the same place as the thing it excuses.

Related documentation