BigQuery Service Account Key in dbt Repo: Find & Fix It
A BigQuery service account key committed to a dbt repo is a live credential, not a tidy-up task. Find it, rotate it and remove it for good.
To find a BigQuery service account key in your dbt repo, search the full git history (not just the working tree) for the string BEGIN PRIVATE KEY, then use gcloud to list every user-managed key on the service account dbt runs as. Treat anything you find as compromised: rotate it, then replace the keyfile with service account impersonation or Workload Identity Federation so no long-lived secret is left to leak.
If you are searching for this, you probably just found a keyfile_json block in a profiles.yml, or a sa-key.json sitting in the repo root. The scale of the wider problem is not small. GitGuardian's State of Secrets Sprawl 2026 report found 28.65 million new hardcoded secrets in public GitHub commits in 2025, a 34% increase year over year. Private repos are no refuge: the same research reports that internal repositories are 6x more likely to contain a hardcoded secret than public ones. Google's own guidance is blunt: avoid using service account keys whenever possible.
This post gives you the detection commands first, then the fix, then the guardrails that stop it coming back. Every command runs against your own repo and project in a few minutes.
Why Does a BigQuery Service Account Key End Up in a dbt Repo?
In our work with early-stage companies, it is almost never carelessness. It is a sequence of reasonable shortcuts:
- The inline profile. A developer sets
method: service-account-jsonand pastes the whole key intokeyfile_jsonso the profile "just works" in CI. dbt searches the current working directory forprofiles.ymlbefore it checks~/.dbt/, so a profile in the project folder gets picked up and committed with everything else. - The temporary keyfile. A
sa-key.jsonis dropped in the repo root for a Friday-afternoon deploy, andgit add .does the rest. - The cleanup commit. Someone notices and deletes the file in a later commit. The working tree is clean. The history is not.
A pattern we see repeatedly is a single service account with BigQuery Admin shared between dbt, the BI tool and somebody's notebook. When that key leaks, the blast radius is the whole project, not one pipeline. That is why the detection step below covers both the key and what it can do.

📺 Watch: BigQuery ODBC Driver - Simba using Service Account
How Do You Find a BigQuery Service Account Key in a dbt Repo?
Start with the repo. Run these from the project root. The second and third commands are the ones people skip, and they are the ones that find the deleted file.
# 1. Current working tree
git grep -nI -e 'BEGIN PRIVATE KEY' -e 'keyfile_json' -e 'private_key_id'
# 2. Every commit on every branch, including files later deleted
git log --all -S'BEGIN PRIVATE KEY' --oneline
git log --all -S'private_key_id' --oneline
# 3. Key-shaped filenames that were ever added
git log --all --diff-filter=A --name-only --pretty=format: | grep -Ei 'keyfile|service.?account|credentials|\.json$' | sort -u
The -S flag (git's pickaxe) returns commits where the number of occurrences of the string changed, so you will normally see two hits: the commit that added the key and the commit that removed it. Note the private_key_id value from the first match. You need it in the next step.
Also check anywhere the repo has been copied: forks, CI caches, other engineers' laptops, and any profiles.yml sitting in a project directory rather than ~/.dbt/. Finding the key in git is only half the job. The question that matters is whether it still works and what it can reach.
Which Keys Are Still Live, and Is Anyone Using Them?
List every user-managed key on every service account in the project. Match the key ID against the private_key_id you found in git.
gcloud iam service-accounts list --project=my-gcp-project --format='value(email)' | while read SA; do
echo "== $SA"
gcloud iam service-accounts keys list --iam-account="$SA" --managed-by=user --format='table(name.basename(),validAfterTime,validBeforeTime)'
done
Two things to read in that output. A key ID that matches your git hit is confirmed live. A validBeforeTime in the year 9999 means the key never expires. Keys created two years ago for a "temporary" job are a common find.
Next, see which service accounts are actually running queries, and how many of those jobs are dbt. dbt-bigquery adds a query comment containing dbt_version by default, which gives you a cheap fingerprint. You need bigquery.jobs.listAll, and you should swap region-eu for your own region.
SELECT
user_email,
COUNT(*) AS jobs_30d,
COUNTIF(STRPOS(query, 'dbt_version') > 0) AS dbt_jobs,
COUNT(DISTINCT DATE(creation_time)) AS active_days,
ROUND(SUM(total_bytes_billed) / POW(1024, 4), 2) AS tib_billed,
MAX(creation_time) AS last_job
FROM `region-eu`.INFORMATION_SCHEMA.JOBS_BY_PROJECT
WHERE creation_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
AND job_type = 'QUERY'
AND user_email LIKE '%gserviceaccount.com'
GROUP BY user_email
ORDER BY dbt_jobs DESC;
Read it like this:
- dbt_jobs close to jobs_30d: a dedicated dbt account. Good. Rotating its key is a contained change.
- dbt_jobs well below jobs_30d: a shared account. dbt, a BI tool or ad-hoc scripts are all using one identity, so you cannot rotate the key without breaking several things at once.
- Jobs with no owner you can name: the most urgent finding in the table.
This view tells you what ran, not which key authenticated. For that, Cloud Audit Logs record the key used on each request. Replace KEY_ID with the ID you found in git:
gcloud logging read 'protoPayload.authenticationInfo.serviceAccountKeyName:KEY_ID' --project=my-gcp-project --freshness=30d --limit=20
If that returns calls from sources you do not recognise, you have an incident rather than a hygiene task. If you also tag dbt runs with query labels, our guide to dbt query labels in BigQuery shows how to make this attribution much sharper.
Running those checks for one repo is fine. Checking every model and profile across a whole project is not. fintel-scan is a free, open-source (MIT) CLI that does this check and fourteen others locally, with no warehouse connection: uvx fintel-scan.
How Do You Replace the Key Without Breaking Production dbt Runs?
Triage first. Not every finding has the same urgency:
- Key in history, and the service account has BigQuery Admin or write access to production: rotate today.
- Key in history, service account is read-only on a few datasets: rotate this week, then reduce its roles.
- Key only in a local, untracked profile: schedule the migration, but do not copy the file anywhere else.
The order of operations matters. Create the replacement authentication and switch your dbt jobs over before deleting the old key. Deleting first is how you get failed 6am runs and a stakeholder asking why the dashboards are empty. Once everything runs keyless, revoke the old key:
gcloud iam service-accounts keys delete KEY_ID --iam-account=dbt-runner@my-gcp-project.iam.gserviceaccount.com
The replacement for local development is OAuth plus impersonation. No key file exists anywhere:
my_project:
target: dev
outputs:
dev:
type: bigquery
method: oauth
project: my-gcp-project
dataset: dbt_dev
impersonate_service_account: dbt-runner@my-gcp-project.iam.gserviceaccount.com
threads: 4
Each developer runs gcloud auth application-default login, and you grant roles/iam.serviceAccountTokenCreator on that one service account. Google advises against granting access to service accounts at the project or folder level, so bind the role on the account itself. For CI, use Workload Identity Federation so GitHub Actions or GitLab exchanges its own identity token for short-lived credentials. Workload Identity Federation lets workloads use credentials from an external identity provider to impersonate a Google Cloud service account, and dbt then uses the same oauth method against the ambient credentials.
Only after revocation should you scrub history with git filter-repo. Scrubbing is hygiene, not remediation. The credential is already out there. This is also the moment to cut the dbt account down to what it needs: data editor on its target datasets and job user on the project, not admin. If you want to go further on read-side isolation, see how dbt authorized views in BigQuery can go wrong.
For a single dbt project with a dedicated service account, the migration is usually an afternoon. Shared accounts take longer, because untangling who uses what is the real work. If you would rather have this done once and properly, explore our services.

How Do You Stop a BigQuery Service Account Key in dbt Coming Back?
Fixing it once is easy. Keeping it fixed needs controls that do not depend on anyone remembering.
1. Block key creation at the organisation level. Google provides organisation policy constraints for exactly this: constraints/iam.disableServiceAccountKeyCreation prevents principals from creating user-managed service account keys. Pair it with the upload constraint so nobody can attach their own key pair. Grant narrow exceptions per project rather than leaving the door open.
2. Scan commits before they land. Run a secret scanner in a pre-commit hook and in CI against full history, for example:
gitleaks detect --source . --log-opts=--all
3. Keep profiles out of the repo. Add profiles.yml and *.json keyfile patterns to .gitignore, and keep the real profile in ~/.dbt/. With impersonation, the profile contains no secret anyway, so it becomes safe to commit.
4. Run the audit on a schedule. Put the gcloud key listing and the JOBS query above into a monthly check. New user-managed keys or an unrecognised service account should be a ticket, not a surprise. It is the same discipline we apply to cost governance, where the failure mode is also silent drift.
Google's guidance also tells teams to use insights and metrics to identify unused service account keys. Unused keys are the cheapest risk to remove, because nothing breaks when they go.
Frequently Asked Questions
Q: Is it safe to commit a service account key to a private repo?
A: No. Private repos get cloned, forked and shared, and the GitGuardian 2026 report found internal repositories are about six times more likely than public ones to contain a hardcoded secret. Google's guidance is not to submit service account keys to source code repositories at all.
Q: How do I remove a service account key from git history?
A: Revoke the key first, because history rewriting does not un-leak it. Then use git filter-repo with the path of the keyfile to remove it from every commit, force-push, and have collaborators re-clone. Forks and cached pull request refs may still hold copies.
Q: What is the best way to authenticate dbt to BigQuery without a keyfile?
A: Use method: oauth with impersonate_service_account for local development, and Workload Identity Federation in CI. On Google-hosted runtimes such as Cloud Run, an attached service account works with no extra configuration. None of these leave a long-lived secret on disk.
Q: How do I know if a leaked BigQuery service account key was used?
A: Query Cloud Audit Logs for the key ID, which is recorded in the authentication info of each request. Then compare against INFORMATION_SCHEMA.JOBS_BY_PROJECT for that service account's recent queries, looking for unfamiliar jobs, odd hours or unexpected data volumes.
Q: Can I block service account key creation across my whole GCP organisation?
A: Yes. Apply the iam.disableServiceAccountKeyCreation and iam.disableServiceAccountKeyUpload organisation policy constraints at the organisation root, then allow narrow exceptions per project where a third-party tool genuinely requires a key.
Conclusion
A BigQuery service account key in a dbt repo is a live credential with a long memory, and the hard part is rarely the swap to impersonation. It is untangling a shared, over-privileged account that dbt, your BI tool and half the team all depend on. At Fintel Analytics, we have helped fast-growing fintech, payments and e-commerce teams audit exactly this kind of exposure and move their dbt and BigQuery stacks to keyless, least-privilege access without disrupting a single scheduled run. If you have just found a key in your git history, the cost of fixing it properly this week is a small fraction of explaining it later to an investor, an auditor or a customer.
