Skip to main content

Securing Your CI/CD Pipeline: Eliminate Long-Lived Credentials with Workload Identity Federation (3)

  • October 17, 2024
  • 2 replies
  • 159 views

David-French
Staff
Forum|alt.badge.img+9

Welcome to the final post in this series on how to improve an organization’s security posture by reducing the risks associated with using long-lived credentials. Part one reviewed the security risks of using long-lived credentials and part two explained how to implement Workload Identity Federation to issue short-lived credentials to workloads, eliminating the need for service account keys.

In today’s post, I’m going to demonstrate how to use Google Security Operations (SecOps) to understand the use of service account keys in your environment and how to detect their creation. If your organization uses service account keys, the migration to replace them with more secure authentication methods might take some time. In the meantime, as defensive practitioners there are some proactive steps we can take to baseline our environment and develop detections that identify unusual behavior. 

It’s also worth noting that if your organization has restricted service account usage (e.g. disabling the creation of service account keys), a rule that detects when a service account key is created should be considered a suspicious or notable event that requires investigation.

Ingesting Google Cloud Logs into Google SecOps

Service account key creation, upload, and authentication events are logged in Google Cloud logs. We need to enable the ingestion of Google Cloud logs into Google SecOps before we can run searches and create rules based on these events. This is accomplished in the Google Cloud console on the “Google Security Operations administration settings” page. Select the Google Cloud organization that contains the project that’s bound to your Google SecOps tenant and enable the options highlighted in the image below.

Enabling the ingestion of Google Cloud Audit logs into Google SecOps

Understanding Service Account Key Usage

Now that Google SecOps is ingesting Google Cloud logs, the following search can be used to understand the service account key usage in my Google Cloud organization. This is a statistical search that counts the number of events in my Google Cloud logs where a service account key was used to carry out an action. The results are grouped by the service account key ID (SA_KEY_ID) and service account email address (SA_EMAIL_ADDRESS).

 

metadata.log_type = "GCP_CLOUDAUDIT" security_result.detection_fields["key_id"] != "" security_result.action = "ALLOW" $sa_email_address = principal.user.email_addresses $sa_key_id = security_result.detection_fields["key_id"] match: $sa_email_address, $sa_key_id outcome: $event_count = count_distinct(metadata.id) order: $event_count desc

 

This search can be used to understand how prevalent service account key use is in a Google Cloud environment and by which service accounts.

Reviewing the results of a statistical search in Google SecOps

Detecting Service Account Key Creation and Uploads

As I mentioned earlier, if your organization has taken steps to prevent people from creating or uploading service accounts in your Google Cloud environment, your security team will likely want to detect that behavior if it happens and investigate the root cause.

The rule below detects when a service account key is successfully created or uploaded in your Google Cloud organization. If there are specific user accounts that are allowed to create service account keys in your environment, this rule can be customized to filter results based on the “principal.user.userid” field for example.

In the outcome section of this rule, the expiration date for the service account key is being parsed into a human readable date and is stored in the $sa_key_expiry_date variable. If you want a deeper dive on timestamp manipulation in Google SecOps, I recommend checking out the post, New to Google SecOps: Time, Time, Time, See What’s Become of Me.

 

rule google_cloud_service_account_key_created_or_uploaded { meta: author = "Google Cloud Security" description = "Detects when a service account key is created in or uploaded to a monitored Google Cloud project." assumption = "Google SecOps is ingesting Google Cloud logs. Reference: https://cloud.google.com/chronicle/docs/ingestion/cloud/ingest-gcp-logs" type = "alert" severity = "Medium" priority = "Medium" platform = "Google Cloud" data_source = "GCP Cloud Audit" mitre_attack_tactic = "Persistence" mitre_attack_technique = "Account Manipulation: Additional Cloud Credentials" mitre_attack_url = "https://attack.mitre.org/techniques/T1098/001/" mitre_attack_version = "v15" reference = "https://cloud.google.com/iam/docs/best-practices-for-managing-service-account-keys" events: $gc.metadata.log_type = "GCP_CLOUDAUDIT" $gc.metadata.product_name = "Google Cloud IAM" ( $gc.metadata.product_event_type = "google.iam.admin.v1.CreateServiceAccountKey" or $gc.metadata.product_event_type = "google.iam.admin.v1.UploadServiceAccountKey" ) $gc.security_result.action = "ALLOW" $gc.security_result.detection_fields["key_id"] = $sa_key_id match: $sa_key_id over 30m outcome: $risk_score = max(65) $mitre_attack_tactic = "Persistence" $mitre_attack_technique = "Account Manipulation: Additional Cloud Credentials" $mitre_attack_technique_id = "T1098.001" $event_count = count_distinct($gc.metadata.id) $principal_ip = array_distinct($gc.principal.ip) $principal_user_userid = array_distinct($gc.principal.user.userid) $principal_ip_country = array_distinct($gc.principal.ip_geo_artifact.location.country_or_region) $principal_ip_state = array_distinct($gc.principal.ip_geo_artifact.location.state) $principal_ip_city = array_distinct($gc.principal.location.city) $security_result_summary = array_distinct($gc.security_result.summary) $sa_key_expiry_date = array_distinct(timestamp.get_date(cast.as_int($gc.about.labels["res_valid_before_time"]))) condition: $gc }

 

The image below shows a detection from the new rule. We can see that a new service account key was created with a key ID starting with “5cff” and an expiration date of 2025-01-08.

Detecting service account key creation in Google SecOps

Wrap Up

That’s a wrap for this blog series where I covered the following:

  • Understanding the security risks of using long-lived secrets
  • How to implement Workload Identity Federation to reduce security risk for an organization and issue short-lived credentials to workloads
  • Using Google SecOps to understand the use of service account keys in a Google Cloud environment and detect service account key creation

Thanks for reading. Feel free to reach out on the Google Cloud Security Community with any questions.

Additional Resources

2 replies

defcesco
Forum|alt.badge.img+1
  • Bronze 2
  • April 30, 2025

Hey @David-French

I recreated your blog post series.

I appreciate that you and your team are releasing more community content. Communities like Elastic, Splunk, Microsoft Sentinel, Sigma, and, hopefully, Google SecOps are rising tides that lift all blue teams. 

I'm testing out updating rules via the rules_config.yaml file:

# Example rule_config.yaml hacktool_purpleknight_execution: enabled: false alerting: false aws_guardduty_brute_force_activity_detected: enabled: true alerting: false aws_guardduty_black_hole_traffic_detected: enabled: true alerting: false


If I modify the rule in the Chronicle UI and then re-run the pipeline, the original rule in my repository should overwrite my changes. However, I am encountering an error when trying to patch. Do you have any idea why I'm getting this error? 

30-Apr-25 12:38:05 UTC | INFO | update_remote_rules | Checking if any rule updates are required 30-Apr-25 12:38:05 UTC | DEBUG | update_remote_rules | Rule aws_guardduty_brute_force_activity_detected (None) - Comparing rule text in local and remote rule 30-Apr-25 12:38:05 UTC | INFO | update_remote_rules | Rule aws_guardduty_brute_force_activity_detected (None) - Rule text is different. Creating new rule version 30-Apr-25 12:38:05 UTC | DEBUG | _make_request | https://us-chronicle.googleapis.com:443 "PATCH /v1alpha/None?updateMask=text HTTP/1.1" 404 1589 30-Apr-25 12:38:05 UTC | WARNING | update_rule | <!DOCTYPE html> <html lang=en> <meta charset=utf-8> <meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width"> <title>Error 404 (Not Found)!!1</title> <style> *{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px} </style> <a href=//www.google.com/><span id=logo aria-label=Google></span></a> <p><b>404.</b> <ins>That’s an error.</ins> <p>The requested URL <code>/v1alpha/None?updateMask=text</code> was not found on this server. <ins>That’s all we know.</ins> Traceback (most recent call last): File "/opt/hostedtoolcache/Python/3.10.17/x64/lib/python3.10/runpy.py", line 196, in _run_module_as_main return _run_code(code, main_globals, None, File "/opt/hostedtoolcache/Python/3.10.17/x64/lib/python3.10/runpy.py", line 86, in _run_code exec(code, run_globals) File "/home/vsts/work/1/s/tools/rule_manager/rule_cli/__main__.py", line 463, in <module> update_remote_rules() File "/home/vsts/work/1/s/tools/rule_manager/rule_cli/__main__.py", line 81, in update_remote_rules rule_updates = Rules.update_remote_rules(http_session=http_session) File "/home/vsts/work/1/s/tools/rule_manager/rule_cli/rules.py", line 633, in update_remote_rules update_rule( File "/home/vsts/work/1/s/tools/rule_manager/google_secops_api/rules/update_rule.py", line 83, in update_rule response.raise_for_status() File "/home/vsts/work/1/s/tools/rule_manager/venv/lib/python3.10/site-packages/requests/models.py", line 1024, in raise_for_status raise HTTPError(http_error_msg, response=self)


 


David-French
Staff
Forum|alt.badge.img+9
  • Author
  • Staff
  • April 30, 2025

@defcesco – I think you'll need to first run the 'pull latest rules' command to retrieve all of the rules and rule config that's currently in your Google SecOps tenant. Then you should be able to modify the rules/rule config locally or in your software development platform (e.g. GitHub, GitLab) before running the 'update remote rules' command or whatever CI/CD pipeline jobs you have for retrieving/updating rules in SecOps.

Can you please open a GitHub issue here if you continue to experience issues? https://github.com/chronicle/detection-rules/issues

 

Thanks