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_exemptholds the check IDs,compliance_exemption_reasonholds the justification,compliance_exemption_expiryholds the date it lapses. - The tag value is colon-separated with no spaces.
KSI-A:KSI-Bworks;KSI-A: KSI-BandKSI-A,KSI-Bare silently ignored. - Tag with a KSI ID to excuse every check under that indicator, or a check ID to excuse exactly one.
- 176 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.
Navigation
Exemptions are applied in your own cloud environment, not inside GRC-ITSM, so there is no module to enable. To confirm one took effect, open the child validation ticket for the check under Compliance > KSIs and read the newest row on its Validations tab.
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.
A deviation attaches to the issue raised by a failing check, so the usual order is: the check fails, the Not Satisfied validation enters the vulnerability workflow, you deviate against that issue, and then you tag. Tagging a resource that has never failed leaves you with a suppression and no governing record.
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. Each resulting ID is then matched independently, case-insensitively. 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. Both apply |
KSI-CNA-IBP: KSI-RPL-ABO.11 |
Partly applied | The space corrupts only the ID it touches. KSI-CNA-IBP still applies in full; KSI-RPL-ABO.11 silently does not |
KSI-CNA-IBP,KSI-RPL-ABO.11 |
Ignored | Comma is not a separator. The whole value is read as one ID, matching nothing |
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 check output says nothing about a tag being present, so the only way to know your tag worked is to confirm the affected resources now report as skipped. Always verify after tagging.
The partial case is the dangerous one. Because IDs match independently, a value like KSI-CNA-IBP: KSI-RPL-ABO.11 does not fail cleanly -- it quietly grants the broader indicator-level exemption you typed first while dropping the specific one you actually cared about. Nothing reports the half that failed, and the half that succeeded suppresses more than you intended. Check both ends of a multi-ID tag, not just that something changed.
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 of the 176 exemptable 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 -- 121 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 |
| Secrets Manager secret | aws_secretsmanager_secret |
KSI-SVC-ACM.11, KSI-SVC-ACM.12, KSI-SVC-ACM.13, KSI-SVC-ASM.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 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) |
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 "azurerm_storage_account" "test_data" {
name = "testdata"
resource_group_name = azurerm_resource_group.test.name
location = azurerm_resource_group.test.location
account_tier = "Standard"
account_replication_type = "LRS"
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"
}
}
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:
Verify an exemption¶
Because every formatting mistake fails silently, confirm the result rather than assuming it.
- Re-run the scan. Wait for the next scheduled run or trigger one by hand.
- Check the resource's status. It should report as
skip, notalarm. If it still fails, the tag did not match -- re-read Getting the format exactly right, and check the tag key's capitalization first. - Confirm the reason came through. The result text should carry your justification. A skip with no reason means
compliance_exemption_reasonis missing or its key is misspelled. - 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 |
| Remember what an exemption does not do | It removes a resource from one check's measurement. It does not remove the resource from your inventory or your authorization boundary, and it does not excuse it from any other check. The resource stays in scope for the System Security Plan and for every indicator it touches |
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
- KSI Tracking -- the indicator and check structure, and how the check IDs used here are formed
- Steampipe/Powerpipe Stack -- the scanner that reads these tags, and how to trigger a run
- Vulnerability Deviation -- the ticket type recording an accepted risk
- Vulnerability Detection & Response -- the deviation types and the governing policy
- Continuous Monitoring & Reporting -- the policy the evidence feeds