Detection rules › Elastic
AWS Credentials Used from GitHub Actions and Non-CI/CD Infrastructure
Detects AWS access keys that are used from both GitHub Actions CI/CD infrastructure and non-CI/CD infrastructure. This pattern indicates potential credential theft where an attacker who has stolen AWS credentials configured as GitHub Actions secrets and is using them from their own infrastructure.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | T1078.004 Valid Accounts: Cloud Accounts |
| Lateral Movement | T1550.001 Use Alternate Authentication Material: Application Access Token |
Rule body elastic
[metadata]
creation_date = "2026/04/21"
integration = ["aws"]
maturity = "production"
updated_date = "2026/04/21"
[rule]
author = ["Elastic"]
description = """
Detects AWS access keys that are used from both GitHub Actions CI/CD infrastructure and non-CI/CD infrastructure.
This pattern indicates potential credential theft where an attacker who has stolen AWS credentials configured as GitHub
Actions secrets and is using them from their own infrastructure.
"""
false_positives = [
"""
AWS credentials legitimately shared between GitHub Actions and another Microsoft/Azure service
may trigger this rule. Verify whether the non-CI/CD source IP is expected for the workload.
""",
"""
GitHub Actions self-hosted runners running on non-Microsoft/Amazon/Google infrastructure will
appear as suspicious. Add the ASN of your self-hosted runner infrastructure to the is_cicd_infra
allowlist.
""",
]
from = "now-7d"
interval = "1h"
language = "esql"
license = "Elastic License v2"
name = "AWS Credentials Used from GitHub Actions and Non-CI/CD Infrastructure"
note = """## Triage and analysis
### Investigating AWS Credentials Used from GitHub Actions and Non-CI/CD Infrastructure
This rule detects when an AWS access key appears in CloudTrail from both GitHub Actions runners
(identified by Microsoft ASN or the `github-actions` user agent string) and from infrastructure
outside the expected CI/CD provider ASNs. This is a strong indicator that AWS credentials stored
as GitHub repository or organization secrets have been exfiltrated and are being used by an
attacker from their own infrastructure.
### Possible investigation steps
- Identify which GitHub repository owns the credential by cross-referencing the access key ID with
your GitHub Actions workflow configurations and AWS IAM user/role assignments.
- Review the suspicious source IPs and ASNs — residential ISPs, VPN providers, or budget hosting
providers are high-confidence indicators of credential theft.
- Check the actions performed from the suspicious source — `sts:GetCallerIdentity` followed by
enumeration calls (`ListBuckets`, `DescribeInstances`, `ListUsers`) is a common attacker recon
pattern after credential theft.
- Review the user agent strings from the suspicious source — `aws-cli` or `boto3` from a non-runner
IP confirms manual/scripted usage outside CI/CD.
- Check GitHub audit logs for recent workflow changes, new collaborators, or secret access events
that could indicate how the credential was stolen.
- Determine if the credential is a long-lived IAM user key or a temporary STS session — temporary
credentials from `AssumeRoleWithWebIdentity` (OIDC) are less likely to be exfiltrated but still
possible.
### Response and remediation
- Immediately rotate the compromised AWS access key in IAM and update the GitHub repository/org secret.
- Review and revoke any resources created or modified by the suspicious source IP using CloudTrail
event history filtered by the access key ID.
- Audit the GitHub repository for signs of compromise — check for unauthorized workflow modifications,
new secrets, or suspicious pull requests that could have exfiltrated the credential.
- Implement OIDC-based authentication (`aws-actions/configure-aws-credentials` with `role-to-assume`)
instead of long-lived access keys to eliminate the credential theft vector entirely.
- If using OIDC, add IP condition policies to the IAM role trust policy to restrict
`AssumeRoleWithWebIdentity` to known GitHub runner IP ranges.
- Enable GitHub's secret scanning and push protection to detect accidental credential exposure in
code or logs.
"""
references = [
"https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services",
"https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc.html",
]
risk_score = 73
rule_id = "b8c7d6e5-f4a3-4b2c-9d8e-7f6a5b4c3d2e"
severity = "high"
tags = [
"Domain: Cloud",
"Data Source: AWS",
"Data Source: Amazon Web Services",
"Data Source: AWS CloudTrail",
"Data Source: AWS IAM",
"Use Case: Threat Detection",
"Tactic: Initial Access",
"Tactic: Lateral Movement",
"Resources: Investigation Guide",
]
timestamp_override = "event.ingested"
type = "esql"
query = '''
from logs-aws.cloudtrail-* metadata _id, _version, _index
| WHERE event.dataset == "aws.cloudtrail"
AND aws.cloudtrail.user_identity.access_key_id IS NOT NULL
AND @timestamp >= NOW() - 7 days
AND source.as.organization.name IS NOT NULL
// AWS API key used from github actions
| EVAL is_aws_github = user_agent.original LIKE "*aws-credentials-for-github-actions"
// non CI/CD related ASN
| EVAL is_not_cicd_infra = not source.as.organization.name IN ("Microsoft Corporation", "Amazon.com, Inc.", "Amazon Technologies Inc.", "Google LLC")
| STATS Esql.is_github_aws_key = MAX(CASE(is_aws_github, 1, 0)),
Esql.has_suspicious_asn = MAX(CASE(is_not_cicd_infra, 1, 0)),
Esql.last_seen_suspicious_asn = MAX(CASE(is_not_cicd_infra, @timestamp, NULL)),
Esql.source_ip_values = VALUES(source.address),
Esql.source_asn_values = VALUES(source.as.organization.name) BY aws.cloudtrail.user_identity.access_key_id, user.name, cloud.account.id
// AWS API key tied to a GH action used from unusual ASN (non CI/CD infra)
| WHERE Esql.is_github_aws_key == 1 AND Esql.has_suspicious_asn == 1
// avoid alert duplicates within 1h interval
AND Esql.last_seen_suspicious_asn >= NOW() - 1 hour
| KEEP user.name, aws.cloudtrail.user_identity.access_key_id, Esql.*
'''
[rule.investigation_fields]
field_names = [
"aws.cloudtrail.user_identity.access_key_id",
"user.name"
]
[[rule.threat]]
framework = "MITRE ATT&CK"
[[rule.threat.technique]]
id = "T1078"
name = "Valid Accounts"
reference = "https://attack.mitre.org/techniques/T1078/"
[[rule.threat.technique.subtechnique]]
id = "T1078.004"
name = "Cloud Accounts"
reference = "https://attack.mitre.org/techniques/T1078/004/"
[rule.threat.tactic]
id = "TA0001"
name = "Initial Access"
reference = "https://attack.mitre.org/tactics/TA0001/"
[[rule.threat]]
framework = "MITRE ATT&CK"
[[rule.threat.technique]]
id = "T1550"
name = "Use Alternate Authentication Material"
reference = "https://attack.mitre.org/techniques/T1550/"
[[rule.threat.technique.subtechnique]]
id = "T1550.001"
name = "Application Access Token"
reference = "https://attack.mitre.org/techniques/T1550/001/"
[rule.threat.tactic]
id = "TA0008"
name = "Lateral Movement"
reference = "https://attack.mitre.org/tactics/TA0008/"
Stages and Predicates
Stage 1: from
from logs-aws.cloudtrail-* metadata _id, _version, _index
Stage 2: where
| WHERE event.dataset == "aws.cloudtrail"
AND aws.cloudtrail.user_identity.access_key_id IS NOT NULL
AND @timestamp >= NOW() - 7 days
AND source.as.organization.name IS NOT NULL
Stage 3: eval
| EVAL is_aws_github = user_agent.original LIKE "*aws-credentials-for-github-actions"
Stage 4: eval
| EVAL is_not_cicd_infra = not source.as.organization.name IN ("Microsoft Corporation", "Amazon.com, Inc.", "Amazon Technologies Inc.", "Google LLC")
Stage 5: stats
| STATS Esql.is_github_aws_key = MAX(CASE(is_aws_github, 1, 0)),
Esql.has_suspicious_asn = MAX(CASE(is_not_cicd_infra, 1, 0)),
Esql.last_seen_suspicious_asn = MAX(CASE(is_not_cicd_infra, @timestamp, NULL)),
Esql.source_ip_values = VALUES(source.address),
Esql.source_asn_values = VALUES(source.as.organization.name) BY aws.cloudtrail.user_identity.access_key_id, user.name, cloud.account.id
Stage 6: where
| WHERE Esql.is_github_aws_key == 1 AND Esql.has_suspicious_asn == 1
AND Esql.last_seen_suspicious_asn >= NOW() - 1 hour
Stage 7: keep
| KEEP user.name, aws.cloudtrail.user_identity.access_key_id, Esql.*
Indicators
Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.
| Field | Kind | Values |
|---|---|---|
Esql.has_suspicious_asn | eq |
|
Esql.is_github_aws_key | eq |
|
Esql.last_seen_suspicious_asn | ge |
|
aws.cloudtrail.user_identity.access_key_id | is_not_null | |
event.dataset | eq |
|
source.as.organization.name | is_not_null |
Output fields
Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.
| Field | Source |
|---|---|
user.name | KEEP user.name |
aws.cloudtrail.user_identity.access_key_id | KEEP aws.cloudtrail.user_identity.access_key_id |
Esql.* | KEEP Esql.* |