This program implements a simple security checker for GCP Service Account Keys for any GCP Service Account using the public x509 certificate endpoint.
It is useful for auditing if GCP Service Accounts used by third party SaaS services are following best pratices before you grant them access to your environment.
All Google Cloud Service Accounts have service account keys associated with them which they use for signing JWTs which can be used as idtokens or exchanged for access tokens. These are almost always 2048-bit RSA keys and are a foundational component of the GCP security model.
These keys have attributes keyOrigin
and keyType
, which can be:
keyOrigin
GOOGLE_PROVIDED
- key material was generated by GoogleUSER_PROVIDED
- generated by the user
keyType
SYSTEM_MANAGED
- key material is managed by GCPUSER_MANAGED
- key material is managed by the user
These can be in the following combinations:
GOOGLE_PROVIDED
/SYSTEM_MANAGED
these are the cloud platform internal SAs that are attached to every Service Account. These keys are used by the methods in the Service Account Credentials REST API likeSignJWT
.GOOGLE_PROVIDED
/SYSTEM_MANAGED
these are created by theprojects.serviceAccounts.keys.create
API and then downloaded to get a "Service Account Key JSON".USER_PROVIDED
/USER_MANAGED
these are created by the user and the certificate portion is uploaded usingprojects.serviceAccounts.keys.upload
API. Google Cloud never has access to these private keys.
Note that USER_PROVIDED
/SYSTEM_MANAGED
doesn't exist because there's no way to import private key material into the cloud.
According to Best practices for managing service account keys it is prefered to not have any USER_MANAGED
keys.
GCP does not directly make the information about what types of keys are attached to a service account public, however, it does provide several endpoints (documented here and here) to see the public portions of the keys, to be used for verifying signatures.
This tool takes the certificates from the public x509 endpoint, and uses heuristics to determine the key origin and type for each key.
Clone this repository and ensure you have golang installed.
If you plan to use features requiring GCP authentication, ensure you run gcloud auth login --update-adc
.
You can run the tool with go run ./... [args]
(or go build
and then ./gcp-sa-key-checker [args]
).
The list of Service Account emails to process can be provided in four different ways:
- On the command line as individual positional arguments
- with the
--in FILE
flag, pointing to a text file with one service account email on each line - with the
--project PROJECTID
flag, which will list all Service Accounts in the project using theprojects.serviceAccounts.list
API - with the
--scope SCOPE
flag, which will list all active service accounts using thesearchAllResources
API. Supported scopes are:projects/{PROJECT_ID}
orprojects/{PROJECT_NUMBER}
(redundant with--project
flag, but requires different permissions)folders/{FOLDER_NUMBER}
organizations/{ORGANIZATION_NUMBER}
The tool can be run in two different modes:
- Normal: Default mode, only list keys that are likely not
GOOGLE_PROVIDED
/SYSTEM_MANAGED
- Verbose: enabled with
--verbose
, it will output all the information about all keys seen. This could be useful for diffing and monitoring but is mostly for debugging. - Ground Truth: enabled with
--ground-truth
, it will use ADCs to pull the real status of the keys from the IAM API, and then compare that with the predictedkeyOrigin
/keyType
from the public information, then it will report any descrepencies. This is useful for verifying the correctness of the heuristics.
Additional flags:
--out-dir DIR
- will write the PEM encoded x509 certificates for all scanned SAs to the output directory--quota-project PROJECT_ID
- will use the specified project for quota/billing purposes. Only really relevant for the--ground-truth
which issues many IAM read calls.
The certificate for each SA key is downloaded using the https://www.googleapis.com/service_accounts/v1/metadata/x509/ACCOUNT_EMAIL
endpoint. Checks are run to gather "Signals" which are a guess towards a specific keyOrigin+keyType combination, and an explanation. The following checks are run, each of which were determined experimentally:
- Validity period (
NotBefore
,NotAfter
)16d12h15m
->GOOGLE_PROVIDED
/SYSTEM_MANAGED
- "two weeks" seems to be a hardcoded value
3650d
(~10 years) ->GOOGLE_PROVIDED
/USER_MANAGED
- legacy user-created SA keys had a 10 year validity
- Valid between
730d
(~2 years) and761d
(~2 years + 1 month) ->GOOGLE_PROVIDED
/SYSTEM_MANAGED
- This does not seem to be documented but was confirmed experimentally. Seems to have started rollout around February 2025
NotAfter
date of9999-12-31 23:59:59 +0000 UTC
->GOOGLE_PROVIDED
/SYSTEM_MANAGED
- SA keys generated after around May 2021 seem to not expire at all
- Any period in the
iam.serviceAccountKeyExpiryHours
org constraint ->GOOGLE_PROVIDED
/USER_MANAGED
- Any other period can not be generated by Google Cloud, so must be
USER_PROVIDED
/USER_MANAGED
- Names (
Subject
andIssuer
)- GAIA IDs ->
GOOGLE_PROVIDED
/USER_MANAGED
- Experimentally determined user-generated keys always have GAIA IDs in these fields.
- service account email or email truncated to 64 bytes ->
GOOGLE_PROVIDED
/GOOGLE_MANAGED
- It is unclear when this truncation occurs, and seems to not be documented.
- Anything else cannot be generated by GCP ->
USER_PROVIDED
/USER_MANAGED
- GAIA IDs ->
- Crypto settings:
- 1024 bit
SHA1WithRSA
->GOOGLE_PROVIDED
/USER_MANAGED
- not sure why anyone would do this, but the API allows it
- anything else other than 2024 bit
SHA1WithRSA
->USER_PROVIDED
/USER_MANAGED
- 1024 bit
- The extensions (key usage, etc) are also checked because these are very consistent from GCP, so if they differ they key must have been
USER_PROVIDED
.
Finally, the signals are compiled, and ordered by precedence. The highest precedence finding wins. The prececdence order is USER_PROVIDED/USER_MANAGED
, GOOGLE_PROVIDED/USER_MANAGED
and finally GOOGLE_PROVIDED/GOOGLE_MANAGED
.
This was run with --ground-truth
across the main Mercari GCP organization which has existed for over 10 years and contains >20k service accounts, including some that have user-generated or user-managed keys. There were no disparities between the heuristic detection code in this script and the ground truth from the API.
Additionally, we pulled data from Wiz for external service accounts that are connected to our environment using the following advanced query:
{
"select": true,
"type": [
"SERVICE_ACCOUNT"
],
"where": {
"_partial": {
"EQUALS": true
},
"externalId": {
"ENDS_WITH": [
".gserviceaccount.com"
]
}
}
}
This discovered that several of our SaaS services are potentially not following GCP best practices for managing service account keys and we plan to privately follow up with them.
The --out-dir
parameter is useful for running keys through badkeys, however we found no examples of such keys in practice. A survey of SA keys looking for issues like duplicate moduli, shared primes or other oddities could be interesting future work, particularly if combined with recon to gather a large number of SAs to scan.
If you want to submit a PR for bug fixes or documentation, please read the CONTRIBUTING.md and follow the instruction beforehand.
The gcp-sa-key-checker is released under the MIT License.