dbt-adapters

Activity, over time

Daily observations from the public event archive.

Attention

Daily stars & forks

Loading attention chart

Star accumulation

Cumulative stars

Loading star accumulation chart

Pull requests

Daily opens & merges

Loading pull requests chart

Conversations

Daily issues & comments

Loading conversations chart

Community

Daily accounts & reviews

Loading community chart

Releases

Daily releases

Loading releases chart

Mentions

2posts · 30d
Nov 40 posts
  1. Qiita · · @hotate_2235

    eMudhra中間CAの秘密鍵漏えいに対して、手元のMacで何をしたか eMudhra中間CAの秘密鍵漏えいに対して、手元のMacで何をしたか 2026年10月20日に、eMudhraの中間CA証明書8枚の秘密鍵が漏えいしたと公表されました。 この記事では、この事象に対して、開発用のMac1台で実際に何を確認し、何を直したかを順に書きます。 :::note info この記事は、2026年10月25日現在におこなった対応をまとめたものであり、それ以降に発見された脆弱性への対応は記載していません。 これなどは未対応です・・・ x.com ::: どんな事件? eMudhraが発行した中間CA証明書8枚について、秘密鍵が誤った方法で生成されていました。 この鍵は証明書の情報から第三者が復元できるため、鍵を手に入れた人はどのWebサイトやサービスにもなりすませます。 該当の証明書は、2025年4月の作成時点から復元可能だったとされています。 (話題になり始めた時のツイート) x.com ブラウザ側はすでに対処済みで、FirefoxとChromeは10月17日、Appleは19日、Microsoftは20日から、この中間CAをブロックしています。 一方で、失効情報を参照しないクライアントは、ソフトウェア自体を更新しないと中間CAを信頼し続けます。 curlやgit、Pythonのpipなど、ブラウザ以外の道具がこれに当たります。 出典は公表文書(eMudhra Intermediate CA Compromise)です。 emudhra.we-have-a-problem.com やるべきことを一言で 信頼できないCAを信頼しないようにする、です。 信頼の判断は、OSの信頼ストア、各言語やツールが同梱するCAバンドル、コンテナ内の信頼ストアの3か所に分散しています。 どこか1か所でも古いままだと、そこ経由の通信だけ侵害された中間CAを受け入れます。 確認には、公表文書にあるテスト用ホストへ2本とも接続します。 2本は別のルートを経由するため、片方だけ直っている状態がありえます。 curl https://dv-g3b-emudhra.we-have-a-problem.com/ # emSign ECC Root CA - G3 経由 curl https://dv-g3b-aaa-emudhra.we-have-a-problem.com/ # AAA Certificate Services 経由 両方とも SSL certificate problem(curlの終了コード60)などの証明書エラーになれば、そのクライアントは問題の中間CAを信頼していません。 HTTP 404や500が返る場合も、TLSは成立しているので未対策です。 curl以外のツールは、同じURLをそのツールの方法で叩いて確かめます(pipなら --index-url に指定するなど)。 対応 OSアップデート macOSを26.7.1に更新し、その後27.0.1へ更新しました。 26の段階で該当ルート2つをcert.pemから除去しました。しかし、27.0.1に更新すると、/etc/ssl/cert.pem が元の状態に戻り、2つのルート(AAA Certificate Services と emSign ECC Root CA - G3)が再び入っていました。 標準のcurlはこのファイルを参照するため、更新後に確認用の2本へ接続できてしまいました。 公表文書の回避策(該当のルート2つをこのファイルから取り除くコマンド)を、更新のたびに再実行する必要があります。 emudhra.we-have-a-problem.com 公表文書のコマンドは、退避してからその場でファイルを上書きします。 実行前に結果を確かめたかったので、一時ファイルに書き出して中身を検証してから置き換える形にしました。 更新のたびに実行するため、退避ファイル名には日付を付け、前回の退避を上書きしないようにしています。 t=$(mktemp) && awk '/^(===|###) /{skip=0} /^=== .*\/CN=(AAA Certificate Services|emSign ECC Root CA - G3)$/{skip=1} !skip' /etc/ssl/cert.pem > "$t" && [ -s "$t" ] && sudo cp -p /etc/ssl/cert.pem "/etc/ssl/cert.pem.bak-$(date +%Y%m%d)" && sudo install -m 0644 -o root -g wheel "$t" /etc/ssl/cert.pem; rm -f "$t" 除くのは AAA Certificate Services と emSign ECC Root CA - G3 の2つです。 片方だけ除くと、除かなかったルート経由の確認用ホストに接続できてしまいます。 実際に、emSign ECC Root CA - G3 だけを除いたファイルでは、dv-g3b-aaa-emudhra への接続が成功しました(curlの終了コード0)。 2つとも除いたファイルでは、2本とも終了コード60になります。 ブラウザのアップデート 公表文書は、ブラウザとOSを最新にするよう案内しています。 ブラウザ側は失効情報の配布機構(OneCRL、CRLite、CRLSets)で対処済みです。 ブラウザ上で先ほどのサイトを開き、did not connectになれば、ブラウザ上は問題ないようです。 !スクリーンショット 2026-10-25 20.53.24.png macOSのネイティブpipなどの対応 macOS標準の curl、git、python3、perl、ruby、openssl は、失効確認をせずに /etc/ssl/cert.pem で証明書を検証します。 このファイルから該当ルートを除く回避策を適用し、curlとgitとpython3が確認用の2本を拒否することを確かめました。 標準のpip3は、このファイルを使わず、自前の古いバンドルを使います。 Command Line Toolsに入っているPython 3.9は、pipを修正版の26.2.2に上げられません。 そこで公表文書の手順どおり、標準のpython3の sitecustomize.py に PIP_CERT=/etc/ssl/cert.pem を設定し、pipに修正済みのファイルを使わせました。 この設定はmacOSの更新後も残っていました。 Homebrewとcertifiの更新 Python環境側は、certifi を2026.10.19、pip を26.2.2に更新しました。 対象は、miniforgeの各環境、asdfで入れたPython、pipenvの仮想環境です。 プロジェクトの Pipfile.lock は古い版を固定したままなので、pipenv sync で古い版に戻ります。 Homebrewは、ca-certificates が2026-10-20版、nss が3.131.1から3.132へ更新済みです。 そのほかのformulaは、公開から1週間以上経った版だけを入れる方針で更新しました。 Rancher Desktop周りの対応 Rancher Desktopは、コンテナ管理用のツールになりますが、これによって作成される環境には、問題が2つありました。 rancherdesktop.io 1つ目は、起動のたびに、macOSのキーチェーンにあるルート証明書がVMへ注入し直される点です。 VM内のAlpineの ca-certificates を修正版(20261020-r0)に上げても、この経路で2つのルートが再び信頼ストアに入りました。 VMを再起動するたびに、ca-certificates の更新、該当ルート2枚の除去、dockerdの再起動を行うスクリプトを実行しています。 実行後に、VM内から確認用の2本が拒否されることを確かめます。 <details><summary>VMの再起動のたびに実行するスクリプト</summary> #!/bin/bash # Rancher Desktop VM の信頼ストアから、問題の2つのルートを外す(VMの再起動のたびに実行) # 使い方: RESTART_DOCKER=1 bash rd_vm_fix.sh set -u RDCTL=$(command -v rdctl 2>/dev/null || echo "$HOME/.rd/bin/rdctl") "$RDCTL" shell true >/dev/null 2>&1 || { echo "Rancher Desktop VM が起動していません"; exit 1; } "$RDCTL" shell sudo sh -c ' # Alpineのca-certificatesを修正版へ更新 apk upgrade -q ca-certificates ca-certificates-bundle 2>/dev/null mkdir -p /root/emudhra-removed # キーチェーンから注入された証明書のうち、問題の2つを退避 for f in /usr/local/share/ca-certificates/*.crt; do s=$(openssl x509 -in "$f" -noout -subject 2>/dev/null) case "$s" in *"CN=AAA Certificate Services"*|*"CN=emSign ECC Root CA - G3"*) echo "remove $(basename "$f"): $s"; mv "$f" /root/emudhra-removed/ ;; esac done update-ca-certificates >/dev/null 2>&1 apk info -v 2>/dev/null | grep "^ca-certificates-[0-9]" # 確認用の2本が拒否される(curlの終了コード60)ことを確認 for h in dv-g3b-emudhra dv-g3b-aaa-emudhra; do curl -sS -o /dev/null --max-time 12 https://$h.we-have-a-problem.com/ 2>/dev/null [ $? -eq 60 ] && echo "OK (拒否) $h" || echo "NG (接続できる/不明) $h" done ' # dockerdは起動時に信頼ストアを読むため、反映にはVM内でdockerサービスの再起動が必要 # (Rancher Desktop自体の再起動だと、また注入し直される) if [ "${RESTART_DOCKER:-0}" = 1 ]; then echo "dockerd を再起動します (実行中コンテナは止まります)" "$RDCTL" shell sudo rc-service docker restart else echo "dockerd に反映するには: RESTART_DOCKER=1 bash $0" fi </details> 2つ目は、コンテナのイメージです。 OSの信頼ストアは、Debian 13とUbuntu 24.04のイメージでは問題ありませんでした。 Alpineは修正版が出ています。 残っていたのは、公式の python:3.11-slim と python:3.12-slim に入っているpip(24.0と25.0.1)が同梱するcertifiでした。 同じイメージの中で、Python本体は確認用ホストを拒否するのに、pipは接続できました。 この2つは配布元の最新イメージでも同じ状態です。再ビルドだけでは直りません。 そこでDockerfileに、OSの信頼ストアを指定してpipを更新する行を足しました。 RUN pip install --no-cache-dir --cert /etc/ssl/certs/ca-certificates.crt -U pip python:3.12系のpip 25.0.1は、--cert を指定しても同梱の古いcertifiを一緒に信頼しました。 3.12系では、この行だけではpipを更新する通信自体が守れません。 同梱のcertifiファイルをOSの信頼ストアで置き換えてからpipを更新する形にしています。 dbt-snowflake周りの対応 dbt-snowflake 1.9.4と1.12.3は、依存関係で certifi<2025.4.26 を要求します。 ベースイメージを最新にしても古いcertifiに固定されるため、依存関係を入れたあとにcertifiを上書きする1行を追加しました。 再ビルドは --pull --no-cache で行います。 キャッシュが効くと、古いaptやpipのレイヤーが残るためです。 Dependabotは FROM のタグしか見ず、RUN の中の apt-get や pip install は追いません。 ちなみに、dbt-snowflakeが古いバージョンを要求する件については、現在は修正済みのようです。 dbt-adapters#2230 dbt-adapters#2241 残り VS Code VS Codeに含まれるElectronが古いままです。 公表文書が示すElectronの修正版は、44.9.1、43.8.1、42.11.16です。 手元のVS Codeは43.7.7で、修正版に届いていません。 VS Codeの更新が配布されるのを待っています。 Claude.app(Electron 44.9.0)も同じ状態です。 Claude.appは、アプリ内に独自の「信頼しない証明書」一覧を持っていることを、アプリの中身で確認しました。 ただし、実際に確認用ホストへ接続して拒否されるかは試せていません。 Rancher Desktop Rancher Desktop本体は、まだ修正版が出ていません。 修正の追跡は、次のissueで行われています(2026年10月25日時点)。 rancher-desktop#11197 このissueは「Open」で、マイルストーンは1.25です。 Development欄にリンクされたブランチやPull Requestはありませんが、このissueを参照するコミットが2件あります。 本体が修正版になるまでは、VMを起動し直すたびに前述のスクリプトを実行します。 まとめ - eMudhraの中間CA 8枚の秘密鍵が復元可能だったため、失効情報を見ないクライアントは、ソフトウェアを更新しないと偽の証明書を受け入れる。 - 確認は、テスト用ホスト2本へ接続して、2本とも証明書エラーになるかで行う。 - OS、各言語のcertifiとpip、コンテナのイメージが、それぞれ別の信頼ストアを持つため、場所ごとに直して確認する。 - 更新で元に戻るものがある。macOS更新後の /etc/ssl/cert.pem と、Rancher Desktop再起動後のVMは、毎回確認する。 - ElectronアプリとRancher Desktop本体は、配布元の修正待ち。 参考情報 - eMudhra Intermediate CA Compromise(2026年10月20日の公表文書) - rancher-desktop#11197

  2. From a Leaked Pipeline Credential to Bulk Data Your dbt job's service account has a key file sitting on a shared drive the early data team used, and another copy in a former contractor's laptop backup. The key has no expiry date. Key creation has been blocked in your organisation for a while, but this key predates the block, and it still gets an access token today. Whoever has the file can use the job's access to your BigQuery datasets, unless a VPC Service Controls perimeter limits where the account's requests can come from. If your platform is older than your key-creation policy, it's worth assuming at least one key like this exists until an inventory says otherwise. In brief. Blocking key creation doesn't retire keys made before the block. A legacy pipeline key can read, and possibly export, curated BigQuery data. BigQuery's Data Access logs, the record of which tables were read, with which key, and from where, stay in the _Default bucket for 30 days unless you change it, so after that you may not be able to say what a key read. This week: export your service account key inventory with Cloud Asset Inventory, find the user-managed keys with no expiry, and check when each one last authenticated. This series treats the data platform's own machinery as the target: the service accounts that run jobs, the orchestrator, CI for SQL and dbt, connectors, keys, and exports. It uses the same example platform as The Lakehouse in Production: Microsoft Entra ID for workforce identity, Google Cloud for the data platform (BigQuery, Datastream, Cloud Composer, and dbt), and Azure SQL Database at source. It's a composite, and it already has the hardening from that series' first part, including an organisation policy that blocks key creation and upload. Part 1 follows one credential to the data it can reach, then goes through the controls that narrow that path, roughly in the order they'd pay off on the example platform. The first one is an inventory, and the rest of the series builds on it. Why blocking key creation didn't fix this Google Cloud has two pairs of organisation policy constraints that stop service account keys at the source. iam.managed.disableServiceAccountKeyCreation and its legacy form iam.disableServiceAccountKeyCreation block new keys. iam.managed.disableServiceAccountKeyUpload and iam.disableServiceAccountKeyUpload block uploaded public keys. Organisations created on or after 3 May 2024 get the legacy pair enforced by default (Google Cloud, n.d.aa). They only apply going forward. Google says they "are not retroactive; they do not affect previously created and configured service accounts" (Google Cloud, n.d.aa). iam.serviceAccountKeyExpiryHours caps key lifetime, but only for keys created after it applies (Google Cloud, n.d.aa). So a key made in the platform's early days keeps working until someone disables or deletes it. We tested this in a sandbox project in October 2026. With creation blocked, a request for a new key failed with FAILED_PRECONDITION. A key we'd made during a short, approved exception still got a fresh token after the block went back on. We used a client that always asks for a new token, because gcloud can return a cached one, which says nothing about whether the key still works. So the hardening stops new keys, subject to any exceptions, and finding the old ones is separate work. From the file to the data Each hop below says what the grant allows and what record it leaves. Figure 1 shows the same path and records. ![Figure 1. A sequence diagram of one legacy key's path across five columns: whoever has the key, Google's token service, BigQuery, audit logs, and a destination bucket. 1: the key file sits on a share or backup, and storing or copying it leaves no record in Google Cloud. 2: it authenticates as the dbt job's account and gets an access token, typically valid for an hour; the Key Authentication Events metric counts the key's authentications. 3: the account holds Data Editor on the datasets and Job User on the project; grant changes are recorded in Admin Activity. 4: query jobs read silver and gold, and BigQuery writes Data Access entries that can carry serviceAccountKeyName, kept 30 days by default in _Default unless changed or routed. 5: data leaves as query results or through an extract job to the bucket; the job gets a Data Access entry, and the bucket's owner logs the write, if at all.](sakurasky.com "Figure 1. One legacy key's path and the record each hop leaves.") Hop 1: the file. The key is a JSON credentials file made for dbt's BigQuery service-account method, which points at it with a keyfile field (dbt Labs, n.d.b). Files like this tend to outlive the projects they were made for. They turn up on a shared drive from a migration, in a laptop backup, or on a branch nobody deleted, and storing or copying them usually leaves no record in Google Cloud. The file store may keep its own access logs, which are worth pulling during an investigation. ATT&CK calls this Credentials In Files, searching "local file systems and remote file shares for files containing insecurely stored credentials" (MITRE, 2026b). Hop 2: the token. Whoever has the file can authenticate as the service account. Each access token typically expires after an hour (Google Cloud, n.d.ab), but the key doesn't, so it can be used again for as long as it stays valid. Each authentication with the key is counted by the Key Authentication Events metric (Google Cloud, n.d.e). Google can automatically disable keys it finds exposed publicly, and that's on by default (Google Cloud, n.d.aa; Google Cloud, n.d.w), but a private share or a backup isn't public, so it's unlikely to help here. From this point the activity looks like the account's own, which ATT&CK files under valid cloud accounts (MITRE, 2026a). Audit log entries for its calls record the key in authenticationInfo.serviceAccountKeyName, next to the account's email in principalEmail, for services that log it. Google notes that some, including App Engine and Compute Engine, don't (Google Cloud, n.d.o). Hop 3: the grants. dbt's documentation lists BigQuery Data Editor, BigQuery User, and BigQuery Job User as "the typical starting point" for dbt access (dbt Labs, n.d.b). Data Editor has bigquery.tables.getData, bigquery.tables.updateData, and bigquery.tables.export, and can be granted on a dataset or a single table. Job User has bigquery.jobs.create and can only be granted on a project, folder, or organisation (Google Cloud, n.d.g). On the example platform the job has Data Editor on the platform's datasets and Job User on the project. BigQuery User would add more, and Part 2 measures how much. A dbt job that builds the silver and gold layers (the cleaned and business-ready tables) usually needs to read the layers below and write its own outputs, so the key can reach most of the curated data. Job User decides where the account can run jobs, not what it can read. bigquery.tables.getData alone lets it read a table's rows through the API's tabledata.list method (Google Cloud, n.d.u), and a query job can be billed to a different project from the one that holds the data (Google Cloud, n.d.f). So removing Job User from your project doesn't contain the key; the dataset grants do. Data Editor can also change and delete those tables, so the key is an integrity risk as well as a confidentiality one. Changes to table data appear as TableDataChange entries in Data Access (Google Cloud, n.d.f). Hop 4: the reads. A query job reads the tables the account can see. ATT&CK's Databases sub-technique covers this kind of collection (MITRE, 2026c). BigQuery records table reads as TableDataRead entries in the Data Access log, including reads through tabledata.list that run no job, and the job itself as JobInsertion and JobChange entries (Google Cloud, n.d.f). When a job is billed to another project, the read is logged in the project that holds the data while the job's own entries go to the billing project, and the billing project's ID is redacted there unless the two projects share a domain or an organisation (Google Cloud, n.d.f). For BigQuery those logs are on by default and land in the _Default bucket unless you've routed them elsewhere (Google Cloud, n.d.i), and _Default keeps them for 30 days unless someone has changed it (Google Cloud, n.d.j). Hop 5: the data leaves. The same grants allow more than queries. Exporting a table to Cloud Storage needs bigquery.tables.export, bigquery.jobs.create, and write access to the destination bucket (Google Cloud, n.d.q). The roles above cover the first two. The third is granted on the bucket, and that bucket can sit in a project or organisation you don't control, so your own allow policies don't limit it. A VPC Service Controls perimeter around BigQuery is the usual control for that path. A Principal Access Boundary policy on the account may also keep it to resources in your organisation, for services that enforce one. Any log of the write to the bucket belongs to the bucket's owner. In ATT&CK, one of the Databases sub-technique's detection analytics names BigQuery among the platforms to watch for unusual table exports (MITRE, 2026c). Transfer Data to Cloud Account covers moving data to another account on the same provider (MITRE, 2025), and applying it to a BigQuery export is our reading. Part 5 covers the export, copy, transfer, and connection paths, and the perimeter controls around them. None of these hops needs a vulnerability; each uses a permission the account already holds. Other credentials like this A legacy key is the clearest example, but the pattern shows up elsewhere. Application-default credentials on an engineer's laptop let tools act as that engineer, with everything the engineer can reach, and the key constraints generally don't apply to them. Later parts cover a short-lived token printed in a CI log (Part 3), an Airflow connection holding a key (Part 4), and a connector vendor's credential (Part 6). Controls that narrow the path These are ordered roughly by how much each one narrows this path on the example platform. The right order for you depends on what you already have, your team, and your tooling, and none of them closes the path on its own. 1. Find the keys you already have The constraint stops new keys. To find the old ones, Cloud Asset Inventory can export service accounts (iam.googleapis.com/ServiceAccount) and their keys (iam.googleapis.com/ServiceAccountKey) as they stood at a point in time you choose (Google Cloud, n.d.p; Google Cloud, n.d.b). Ours came out as newline-delimited JSON, one asset per line. Each key record had its type, origin, and validity window, and disabled keys had "disabled": true. Listing 1 reads that file. For each user-managed key it reports the key's age, whether it ever expires, whether it's disabled or was uploaded, and whether its account is in the same export. It also writes out the graph the series builds on: accounts and keys as nodes, a disabled flag on each key, and an edge from each key to the account it authenticates as. It skips Google-managed keys. Google holds their private halves in escrow and nobody can access them directly (Google Cloud, n.d.ab), and using them to act as an account is impersonation, which Part 2 covers. Python """Credential inventory over a Cloud Asset Inventory export (Part 1). Reads newline-delimited JSON exported with --content-type=resource and emits the series graph format: principals, credentials, and the edges between them, plus one finding per user-managed key. """ import json import sys from datetime import datetime, timezone SERVICE_ACCOUNT = "iam.googleapis.com/ServiceAccount" KEY = "iam.googleapis.com/ServiceAccountKey" def parse_time(value): # validAfterTime looks like 2023-03-02T09:20:11Z; a bare date means UTC midnight parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc) def inventory(lines, as_of): accounts, keys = {}, [] for line in lines: if not line.strip(): continue asset = json.loads(line) data = asset["resource"]["data"] if asset["asset_type"] == SERVICE_ACCOUNT: accounts[data["uniqueId"]] = data["email"] elif asset["asset_type"] == KEY and data.get("keyType") == "USER_MANAGED": parent_id = asset["resource"]["parent"].rsplit("/", 1)[1] keys.append((parent_id, data, asset.get("ancestors") or [])) nodes = {} for email in accounts.values(): principal = f"serviceAccount:{email}" nodes[principal] = {"id": principal, "kind": "principal", "attrs": {"in_export": True}} edges, findings = [], [] for parent_id, data, ancestors in keys: email = data["name"].split("/serviceAccounts/")[1].split("/keys/")[0] principal = f"serviceAccount:{email}" credential = f"key:{data['name']}" in_export = parent_id in accounts # A key with no end time is treated as never expiring, so it's reported ends = data.get("validBeforeTime", "9999-") no_expiry = ends.startswith("9999-") nodes.setdefault(principal, {"id": principal, "kind": "principal", "attrs": {"in_export": in_export}}) nodes[credential] = {"id": credential, "kind": "credential", "attrs": {"key_type": "USER_MANAGED", "disabled": bool(data.get("disabled"))}} edges.append({"from": credential, "to": principal, "type": "authenticates_as", "evidence": "asset-export"}) findings.append({ "key": data["name"], "account": email, "age_days": (as_of - parse_time(data["validAfterTime"])).days, "no_expiry": no_expiry, "expired": not no_expiry and parse_time(ends) <= as_of, "disabled": bool(data.get("disabled")), "uploaded": data.get("keyOrigin") == "USER_PROVIDED", "account_in_export": in_export, # Audit logs name the key with this prefix (serviceAccountKeyName) "log_key_name": f"//iam.googleapis.com/{data['name']}", "project": data["name"].split("/")[1], "ancestors": ancestors, }) return { "format": "pas-graph/1", "nodes": sorted(nodes.values(), key=lambda n: n["id"]), "edges": sorted(edges, key=lambda e: (e["from"], e["to"], e["type"])), "findings": sorted(findings, key=lambda f: f["key"]), } if __name__ == "__main__": path, as_of = sys.argv[1], parse_time(sys.argv[2]) with open(path, encoding="utf-8") as export: print(json.dumps(inventory(export, as_of), indent=2, sort_keys=True)) Rust //! Credential inventory over a Cloud Asset Inventory export (Part 1). //! //! Reads newline-delimited JSON exported with --content-type=resource and //! emits the series graph format: principals, credentials, and the edges //! between them, plus one finding per user-managed key. use chrono::{DateTime, NaiveDate, Utc}; use serde_json::{json, Value}; use std::collections::BTreeMap; const SERVICE_ACCOUNT: &str = "iam.googleapis.com/ServiceAccount"; const KEY: &str = "iam.googleapis.com/ServiceAccountKey"; pub fn parse_time(value: &str) -> DateTime<Utc> { // validAfterTime looks like 2023-03-02T09:20:11Z; a bare date means UTC midnight match DateTime::parse_from_rfc3339(value) { Ok(t) => t.with_timezone(&Utc), Err(_) => NaiveDate::parse_from_str(value, "%Y-%m-%d").expect("RFC 3339 timestamp or date") .and_hms_opt(0, 0, 0).unwrap().and_utc(), } } fn text<'a>(v: &'a Value, field: &str) -> &'a str { v[field].as_str().unwrap_or("") } pub fn inventory<'a>(lines: impl Iterator<Item = &'a str>, as_of: DateTime<Utc>) -> Value { let mut accounts = BTreeMap::new(); let mut keys = Vec::new(); for line in lines.filter(|l| !l.trim().is_empty()) { let asset: Value = serde_json::from_str(line).expect("one JSON asset per line"); let data = asset["resource"]["data"].clone(); match text(&asset, "asset_type") { SERVICE_ACCOUNT => { accounts.insert(text(&data, "uniqueId").to_string(), text(&data, "email").to_string()); } KEY if text(&data, "keyType") == "USER_MANAGED" => { let parent = text(&asset["resource"], "parent"); let (_, parent_id) = parent.rsplit_once('/').expect("parent is a resource name"); let ancestors = asset["ancestors"].clone(); keys.push((parent_id.to_string(), data, ancestors)); } _ => {} } } let mut nodes = BTreeMap::new(); for email in accounts.values() { let principal = format!("serviceAccount:{email}"); nodes.insert(principal.clone(), json!({"id": principal, "kind": "principal", "attrs": {"in_export": true}})); } let (mut edges, mut findings) = (Vec::new(), Vec::new()); for (parent_id, data, ancestors) in &keys { let name = text(data, "name"); let email = name.split("/serviceAccounts/").nth(1).expect("key name holds its account") .split("/keys/").next().unwrap_or(""); let principal = format!("serviceAccount:{email}"); let credential = format!("key:{name}"); let in_export = accounts.contains_key(parent_id); let created = parse_time(text(data, "validAfterTime")); // A key with no end time is treated as never expiring, so it's reported let ends = data["validBeforeTime"].as_str().unwrap_or("9999-"); let no_expiry = ends.starts_with("9999-"); nodes.entry(principal.clone()).or_insert_with(|| json!({"id": principal, "kind": "principal", "attrs": {"in_export": in_export}})); nodes.insert(credential.clone(), json!({"id": credential, "kind": "credential", "attrs": {"key_type": "USER_MANAGED", "disabled": data["disabled"].as_bool().unwrap_or(false)}})); edges.push(json!({"from": credential, "to": principal, "type": "authenticates_as", "evidence": "asset-export"})); findings.push(json!({ "key": name, "account": email, // Whole days, rounded down, to match Python's timedelta.days "age_days": (as_of - created).num_microseconds().expect("age in range") .div_euclid(86_400_000_000), "no_expiry": no_expiry, "expired": !no_expiry && parse_time(ends) <= as_of, "disabled": data["disabled"].as_bool().unwrap_or(false), "uploaded": text(data, "keyOrigin") == "USER_PROVIDED", "account_in_export": in_export, // Audit logs name the key with this prefix (serviceAccountKeyName) "log_key_name": format!("//iam.googleapis.com/{name}"), "project": name.split('/').nth(1).unwrap_or(""), "ancestors": if ancestors.is_array() { ancestors.clone() } else { json!([]) }, })); } let order = |v: &Value, f: &str| v[f].as_str().unwrap_or("").to_string(); edges.sort_by_key(|e| (order(e, "from"), order(e, "to"), order(e, "type"))); findings.sort_by_key(|f| order(f, "key")); json!({ "format": "pas-graph/1", "nodes": nodes.into_values().collect::<Vec<_>>(), "edges": edges, "findings": findings, }) } Listing 1. A service account key inventory over a Cloud Asset Inventory export, emitting the series graph format. If you run Security Command Center Premium, some of this already reaches you as findings. Security Health Analytics flags user-managed keys (USER_MANAGED_SERVICE_ACCOUNT_KEY) and keys that haven't been rotated for more than 90 days (SERVICE_ACCOUNT_KEY_NOT_ROTATED) (Google Cloud, n.d.af). Event Threat Detection flags service accounts used from a new geography or with a new user agent (Persistence: New Geography, Persistence: New User Agent) with an organisation-level activation, though both rules run on Admin Activity logs, so a key that only reads BigQuery may not trigger them (Google Cloud, n.d.y). Use them if you have them. Listing 1 does a different job. It runs on an export without a Premium activation, the export is a file you can keep and compare over time, and its output is the graph the rest of the series uses to show what a key can reach. Part 2 compares that graph with Policy Analyzer and with SCC's attack path simulation (Google Cloud, n.d.x). Run against the example platform, the first finding is the key this post is about: user-managed, on the dbt job's account, created years ago, with a validBeforeTime of 9999-12-31T23:59:59Z. In our export, that end date marked a key with no expiry. One way to retire it is to move the job to another way of authenticating (control 2), run it on the new credential through at least one full schedule cycle, confirm the old key has stopped authenticating, then disable it. Until you delete the key, re-enabling it is your rollback. To check when a key last authenticated, Google points to the Key Authentication Events metric (Google Cloud, n.d.e). Activity Analyzer's serviceAccountKeyLastAuthentication gives a last-authentication date per key, but it's in Preview, it's queried per project with up to ten keys per filter, and it may miss very recent events (Google Cloud, n.d.ae). If a key was used at times that don't fit the dbt job's schedule, or the audit logs show it used from somewhere the job doesn't run, look into that before anything else. Listing 2 exports the organisation's service accounts and keys. Listing 1 expects --content-type=resource, as shown. Shell gcloud asset export --organization=ORG_ID --content-type=resource \ --asset-types=iam.googleapis.com/ServiceAccount,iam.googleapis.com/ServiceAccountKey \ --output-path=gs://BUCKET/keys.ndjson Listing 2. Exporting service accounts and their keys for Listing 1. Placeholder values. A few things trip this up in larger organisations. The caller needs cloudasset.assets.exportResource at the organisation, which the Cloud Asset Viewer role includes (Google Cloud, n.d.h). The destination bucket can't have a retention policy and can't be encrypted with customer-managed Cloud KMS keys, so an organisation that enforces either needs a dedicated, locked-down bucket for this. The export takes time to finish: wait for it before copying the file, and expect a second export to the same path to be rejected until the first one finishes or 15 minutes pass (Google Cloud, n.d.p). Copy the file down from the bucket, then give Listing 1 the file and a date or RFC 3339 time to measure against, for example python3 inventory.py keys.ndjson 2026-10-15. The Python version needs 3.11 or later. Neither version validates its input. Both expect well-formed records, which is what the export writes, and a partial download can cause a parse error. Treat the export file as sensitive, since it's a map of your accounts and keys. Each finding also carries expired, the key's project and ancestors so you can route it to an owner, and log_key_name, the key's name in the form audit logs use. To list live, enabled keys oldest first: jq '.findings | map(select((.expired or .disabled) | not)) | sort_by(-.age_days)'. The field names come from a real export. Top-level fields are snake_case (asset_type, update_time), but fields inside resource.data use the IAM API's camelCase (keyType, validAfterTime). A loader that assumes one style will miss fields. Our sandbox test turned up three limits worth designing around. - Exports lag. An export we took within ten minutes of creating a key and its account had neither. One taken about an hour later had both. That's why Listing 1 reports a key whose account is missing instead of dropping it, and why containment is better confirmed in the IAM API than in an export. - Google-managed keys didn't show up. The IAM API listed one for the test account, and Asset Inventory never did. Listing 1 only reports user-managed keys, so it doesn't matter here, but don't treat the export as a complete list of keys. - The export doesn't say why a key was disabled. After we disabled ours, the IAM API showed disableReason: SERVICE_ACCOUNT_KEY_DISABLE_REASON_USER_INITIATED, while the inventory record only had disabled: true. If the reason matters, for example after an exposure, read the key from the IAM API, which also has an extendedStatus for exposed or compromised keys. So Listing 1 is only as complete as the export behind it. It can miss keys created shortly before the export, keys in projects outside the export's scope, and credentials that live outside Google Cloud. Treat a finding as a lead to confirm in the IAM API. A clean run tells you about that export, and doesn't prove there are no keys. 2. Get the job off the key file Which approach fits depends on where the job runs, and one platform often needs more than one: - Scheduled runs inside Google Cloud, such as on Cloud Composer: a service account attached to the runtime. In the dbt-bigquery 1.x adapter, method: oauth picks up application default credentials, so there's no key file to store (dbt Labs, n.d.c). Part 4 covers the Composer side. - CI on GitHub Actions: Google documents Workload Identity Federation for deployment pipelines, where a workflow uses its GitHub OIDC token to get short-lived Google Cloud credentials. Google requires an attribute condition that restricts tokens to your GitHub organisation, and recommends the numeric *_id claims over names (Google Cloud, n.d.m). For a production data role, it's worth narrowing the condition to the repository and branch that run dbt. Part 3 covers CI. - Engineers' laptops: each engineer's own login, with impersonate_service_account pointing at a development account if you want local runs to use one identity (dbt Labs, n.d.c). Impersonation needs a permission held by roles such as Service Account Token Creator (Google Cloud, n.d.ad), so grant it on the development account only, never on the production job's account. - A runner outside Google Cloud whose identity lives in Entra: method: external-oauth-wif, shown in Listing 3. Where that runner is an Azure VM with a managed identity, Google documents federation from the managed identity's token, which avoids a stored secret (Google Cloud, n.d.l). dbt doesn't document that combination, so test it before relying on it. dbt v2 documents external-oauth-wif with Microsoft Entra ID (dbt Labs, n.d.b), dbt-bigquery's changelog lists a Workload Identity Federation method from the 1.10.0 beta (dbt Labs, n.d.a), and the adapter's source uses the same external-oauth-wif method name (dbt Labs, n.d.c). So dropping the key file may not need a dbt v2 upgrade; check your installed version. Either way, dbt swaps an Entra-issued token for short-lived Google credentials, which needs a workload identity pool and provider configured to trust your Entra application (Google Cloud, n.d.l). YAML # Before: dbt 1.x, service account key file platform: target: prod outputs: prod: type: bigquery method: service-account keyfile: /path/to/dbt-transform-key.json project: example-data-platform dataset: silver location: EU # After: Workload Identity Federation with Microsoft Entra (dbt v2 syntax; check dbt-bigquery 1.10+ for 1.x) platform: target: external_runner outputs: external_runner: type: bigquery method: external-oauth-wif database: example-data-platform schema: silver location: EU workload_pool_provider_path: //iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID token_endpoint: type: entra request_url: https://login.microsoftonline.com/TENANT_ID/oauth2/v2.0/token request_data: "grant_type=client_credentials&client_id={{ env_var('ENTRA_CLIENT_ID') }}&client_secret={{ env_var('ENTRA_CLIENT_SECRET') }}&scope=SCOPE/.default" Listing 3. A dbt profile before (key file) and after (Workload Identity Federation with Entra, in the syntax dbt v2 documents) for a runner outside Google Cloud. Placeholder values throughout. The old profile uses the older field names project and dataset. The 1.x adapter maps them to database and schema (dbt Labs, n.d.c); on v2, use the new names. dbt's documented example authenticates to Entra with a client secret, so a stored credential remains. It now lives on the Entra side, and that secret is what you protect, rotate, and inventory. Entra also accepts a certificate or a federated credential instead of a shared secret for this kind of request (Microsoft, n.d.), but dbt doesn't document either for external-oauth-wif, so check your version before relying on them. Keep the secret in the runtime's secret store and out of development targets, and give it an expiry on the Entra side. Listing 1 won't find it, so it needs its own line in your inventory. What you get in return is Google credentials that are short-lived and issued per run, with the trust sitting in a pool provider whose conditions you control. Restrict who the provider accepts, with an attribute condition that accepts only the dbt job's own Entra identity, or with app role assignments on the Entra application. Google notes that with Azure's default settings, a new application lets every user in the tenant get access tokens for it (Google Cloud, n.d.l). The federated identity can hold BigQuery roles directly, or the profile can impersonate a service account through the optional service_account_impersonation_url (dbt Labs, n.d.b). If the profile impersonates, use a new service account, not the one with the legacy key. Federation doesn't retire that key, and while the old account stays in the path you may not be able to disable it without stopping the job. Parts 2 and 3 come back to pool conditions and CI identities. Plan this as a joint change. Entra administrators register the application, own its secret or certificate, and limit its app role assignments. The Google Cloud IAM team creates the pool and provider, writes the attribute condition, and grants roles to the federated identity. The data team updates the profile for each target and runs the cutover. 3. Keep Google's leaked-key response on iam.serviceAccountKeyExposureResponse decides what happens when Google detects that a service account key has been "exposed publicly". If it isn't set, it defaults to DISABLE_KEY, which "automatically disables any publicly exposed service account key, and creates an entry in the audit log" (Google Cloud, n.d.w). That has been the default for new and existing customers since 16 June 2024 (Google Cloud, n.d.aa; Liu and Martucci, 2024), and Google says it disables keys found in public repositories in partnership with secret-scanning programmes at sites including GitHub and GitLab (Liu and Martucci, 2024). It only covers public exposure, and Google says it "doesn't guarantee that it will detect leaked keys" (Google Cloud, n.d.e). The key in this post was never public, so the response wouldn't see it. As with a manual disable, tokens already issued from a key it disables may stay valid until they expire (control 6). Check nobody has set the policy to WAIT_FOR_ABUSE, and consider setting DISABLE_KEY explicitly so the behaviour doesn't depend on a default. 4. Make the keys you keep expire Some integrations still need a key. For those, iam.serviceAccountKeyExpiryHours caps the lifetime of new keys, picked from a fixed list running from one hour to 2,160 hours (Google Cloud, n.d.aa; Google Cloud, n.d.w). It does nothing for keys that already exist, so it sits alongside the inventory and doesn't replace it. 5. Keep BigQuery's access logs past 30 days BigQuery's Data Access entries are what tell you which tables a credential read. By default they sit in _Default for 30 days (Google Cloud, n.d.i; Google Cloud, n.d.j). Admin Activity entries, such as a key being created or disabled, go to _Required and stay for 400 days, which you can't change (Google Cloud, n.d.i; Google Cloud, n.d.j). BigQuery's INFORMATION_SCHEMA.JOBS view keeps job history for the past 180 days, including the account that ran each job, its query text, and the tables it referenced (Google Cloud, n.d.t). It only covers jobs run in that project and region, it doesn't record the key or the caller's IP address, and reads through tabledata.list don't create jobs at all. If a key was misused a few months before anyone noticed, you may be able to see that the key authenticated and which queries the account ran, but not where each request came from, which is usually what separates the key holder's activity from the job's own. If you want to answer what a credential read beyond 30 days, route BigQuery's audit logs, Data Access included, somewhere with retention set for investigations. That can be a user-defined log bucket, with retention from 1 to 3,650 days and an irreversible lock if you want one (Google Cloud, n.d.k), or a BigQuery dataset you can query. It costs money, and the routing becomes one more thing to protect. Part 8 comes back to that, since the detection path is a pipeline too. For regulated firms, this gap has a compliance side too. In the EU, DORA asks financial entities to identify, classify, and document their ICT assets (Article 8(1)) and to limit logical access to what's required for approved functions (Article 9(4)(c)) (European Union, 2022). The supporting technical standard asks them to set a retention period for their logs and to log access control and identity management events (Article 12) (European Commission, 2024). In the US, New York's cybersecurity regulation asks covered entities to review all user access privileges at least annually and to promptly terminate access following departures (23 NYCRR 500.7(a)(4) and (a)(6)) (Legal Information Institute, n.d.). A key nobody has inventoried, and logs that expire before an investigation starts, may be hard to square with expectations like these. How they apply depends on your entity and your regulator. BigQuery is unusual in having Data Access logs on by default (Google Cloud, n.d.i). Token requests for other service accounts, such as GenerateAccessToken, are Data Access entries in the Service Account Credentials API's logs, and you turn those on by enabling Data Access logs for the IAM API or for all services (Google Cloud, n.d.ac). Without them, Part 2's question of which accounts a key could act as has no records behind it. 6. If you find a key that's still live Disabling a key stops it authenticating. In our test, a disabled key and a deleted key failed the same way, with invalid_grant: Invalid JWT Signature., so the error alone won't tell you which happened. Google also notes that disabling a key "does not revoke short-lived credentials that were issued based on the key" (Google Cloud, n.d.n). Google says the way to revoke those early is to disable or delete the service account itself (Google Cloud, n.d.n), which stops every workload using it. The sequence below is one to adapt to your own runbooks and roles. Keep the order, except where step 4 says otherwise: 1. Open an incident and an emergency change. Tell the data platform on-call and the owners of downstream data, since disabling credentials will fail scheduled jobs. Confirm the responder holds the roles to disable keys and service accounts, and record each key's current state before changing anything. 2. Start preserving evidence. Copy the account's existing Data Access entries out of _Default, for example with gcloud logging copy to a Cloud Storage bucket, and add a sink for new entries, since a sink doesn't pick up entries stored before it existed (Google Cloud, n.d.d). Admin Activity entries stay in _Required for 400 days, so they can wait. A copy takes at least an hour, so let it run while you contain. Also export the account's rows from INFORMATION_SCHEMA.JOBS in each project and region it runs jobs in, which needs bigquery.jobs.listAll, since those age out after 180 days. 3. Confirm in the IAM API that the key is live (gcloud iam service-accounts keys list --iam-account=SA_EMAIL --managed-by=user), and check when it last authenticated (Google Cloud, n.d.e). 4. Decide whether you can wait, and name in your runbook who makes that call, because it trades pipeline downtime against continued exposure. If step 3 shows use that doesn't fit the job's schedule or runtime, or you can't rule that out, go to step 5 now and accept that the job stays down until step 6 is done. Containment doesn't delete logs, so don't wait for step 2 to finish. Otherwise, do step 6 first, then step 5, with a deadline in days, and alert on any use of the key until it's disabled. 5. Disable every user-managed key on the account, not just the one you found. On the can't-wait branch, disable the service account too, since disabled keys leave tokens already issued valid until they expire (Google Cloud, n.d.n). On the other branch, once nothing else uses the account, disabling it as well closes the remaining token window. 6. Move the job to a different principal (control 2). If control 2 isn't already in place, this is planned work through change control, not an incident step. Don't create a new key as a stopgap. 7. Review what the account did while the key was live: Data Access and Admin Activity entries for that serviceAccountKeyName, tokens it requested for other service accounts, and anything it created or changed that runs as it. Include TableDataRead entries in each project that holds data, since reads by jobs billed elsewhere are logged only there. If it did get tokens for other service accounts, contain those accounts too, because disabling this one doesn't revoke tokens issued to them. 8. Check who could recreate the exposure: who can create keys for the account, who can impersonate it, and who can grant an exception to the key-creation constraint. 9. Delete the disabled keys once nothing needs them, the investigation included, and treat deletion as final. For a key exposed in a repository, Google says to delete it in IAM as quickly as possible (Google Cloud, n.d.e). For a planned cutover, the rollback until step 9 is to re-enable what you disabled. On the can't-wait branch, re-enabling the key or the account hands the holder their access back too, so roll forward to step 6 instead. Disabled keys can only be re-enabled with gcloud or the API, not the console (Google Cloud, n.d.n). Where Part 2 takes the graph Listing 1's output has three parts: nodes (principals, credentials, and later resources), edges with a type and an evidence source, and findings. Part 1 adds one edge type, authenticates_as, from a key to its account. Part 2 adds edges for what an account can reach and whom it can impersonate, plus a traversal over them. Later parts add CI identities, DAG buckets, export paths, connector credentials, decrypt rights, and the log sinks themselves. The graph shows possible reach. Where a deny policy, a Principal Access Boundary policy, or an IAM Condition might block a step, Part 2's traversal will mark the step as possibly blocked and leave the question open. On the edges it has, that means it over-reports, which suits an audit, but it can't report edges its inputs don't contain. Google's Policy Analyzer answers who has what access under allow policies, though Google notes its results don't account for deny policies (Google Cloud, n.d.a). Part 2 compares the two, case by case. What we tested, and what to check yourself We ran the key checks in a sandbox project with no client data, in October 2026, under a short, approved exception to the key-creation policy. Besides the token, export-lag, and disable results described above, we confirmed that: - the export uses the field names and mixed casing described above, a key with no expiry carries an end time of 9999-12-31T23:59:59Z, and the disabled field only appears once a key is disabled; - the two constraint forms report a denied key creation differently. The legacy one returns a PreconditionFailure naming constraints/iam.disableServiceAccountKeyCreation, and the managed one returns an ErrorInfo with reason CUSTOM_ORG_POLICY_VIOLATION. In your own environment, check the dbt job's actual grants, which may be narrower or wider than dbt's starting point. Check whether anything requested tokens for other service accounts during an exposure, since disabling the key doesn't revoke tokens issued to other accounts. Then check the Entra side of the federation, including where the client secret lives and which identities the provider's condition accepts. What to alert on | Event | Where it's seen | |---|---| | A service account key created or uploaded (google.iam.admin.v1.CreateServiceAccountKey, UploadServiceAccountKey) | Admin Activity (Google Cloud, n.d.s) | | A key disabled, enabled, or deleted (DisableServiceAccountKey, EnableServiceAccountKey, DeleteServiceAccountKey) | Admin Activity (Google Cloud, n.d.s) | | A key disabled by the exposure response: DisableServiceAccountKey by gcp-compromised-key-response@system.gserviceaccount.com | Admin Activity (Google Cloud, n.d.aa; Google Cloud, n.d.s) | | A change to an organisation policy on the key constraints, including a project-level exception or the exposure response set to WAIT_FOR_ABUSE (google.cloud.orgpolicy.v2.OrgPolicy.CreatePolicy, UpdatePolicy, DeletePolicy, and the older cloudresourcemanager.v1 setOrgPolicy and clearOrgPolicy on organisations, folders, and projects) | Admin Activity (Google Cloud, n.d.v; Google Cloud, n.d.z) | | Before a job moves off its key: a call whose protoPayload.authenticationInfo.serviceAccountKeyName matches a finding's log_key_name, from a caller IP or user agent the dbt runtime doesn't use. Build the baseline from the runtime's actual callerIp values, and don't treat gce-internal-ip as safe, since a VM outside your organisation can show it too. The user agent is supplied by the client, so treat it as a hint | Data Access and Admin Activity, where the service logs the key name (Google Cloud, n.d.o; Google Cloud, n.d.c) | | After a job moves off its key: any authentication with that key | The Key Authentication Events metric (iam.googleapis.com/service_account/key/authn_events_count, by key_id), alerting on any count; data can take up to three hours to appear (Google Cloud, n.d.e; Google Cloud, n.d.r) | | A key's account requesting tokens for another service account (GenerateAccessToken and related methods) | Service Account Credentials Data Access logs, once enabled (Google Cloud, n.d.ac) | | A BigQuery extract job to a bucket that isn't on your list | BigQuery Data Access: the job's own entries when it runs in your project, and TableDataRead entries on your datasets for reads by jobs run elsewhere (Google Cloud, n.d.f); for buckets outside your organisation, Event Threat Detection's BigQuery data extraction rule where you run it (Google Cloud, n.d.y); Part 5 covers exports in detail | | A user-managed key with no expiry, or older than your rotation limit | Listing 1, run on a schedule | The ATT&CK mapping The mapping uses MITRE ATT&CK v20.0, released on 13 October 2026, and only covers the techniques in this post, plus the key creation the alerts above watch for. | Step | Technique | Tactic | |---|---|---| | The key file on a share or in a backup | T1552.001 Unsecured Credentials: Credentials In Files (MITRE, 2026b) | Credential Access | | A new key on a pipeline account | T1098.001 Account Manipulation: Additional Cloud Credentials (MITRE, 2026d) | Persistence and Privilege Escalation | | Acting as the service account | T1078.004 Valid Accounts: Cloud Accounts (MITRE, 2026a) | Initial Access, Persistence, Privilege Escalation, and Stealth | | Query jobs reading silver and gold | T1213.006 Data from Information Repositories: Databases (MITRE, 2026c) | Collection | | An export to a bucket the holder controls | T1537 Transfer Data to Cloud Account (MITRE, 2025), our reading for BigQuery | Exfiltration | This week Run the inventory, then take these questions to your platform team: 1. How many user-managed service account keys exist across the organisation, and how many never expire? 2. Which of them sit on accounts that run data pipelines, and when did each last authenticate? 3. Do we keep BigQuery Data Access logs for longer than 30 days, and where? 4. Is BigQuery inside a VPC Service Controls perimeter, and would it stop an export to a bucket outside our organisation? 5. If we move to Workload Identity Federation with Entra, where does the client secret live, who rotates it, and which identities does the provider accept? 6. Is the key exposure response explicitly set to DISABLE_KEY? Part 2 looks at the account itself: what the dbt job's account can reach beyond its own datasets, which other accounts it can act as, and how to work that out from the same export. References dbt Labs, n.d.a. CHANGELOG, dbt-bigquery adapter. GitHub. Available at: <github.com/dbt-labs/dbt-adapters/blob/main/dbt-bi…> [Accessed 22 October 2026]. dbt Labs, n.d.b. Connect BigQuery to dbt v2. dbt Developer Hub. Available at: <docs.getdbt.com> [Accessed 15 October 2026]. dbt Labs, n.d.c. credentials.py, dbt-bigquery adapter. GitHub. Available at: <github.com/dbt-labs/dbt-adapters/blob/main/dbt-bi…> [Accessed 15 October 2026]. European Commission, 2024. Commission Delegated Regulation (EU) 2024/1774 of 13 March 2024 supplementing Regulation (EU) 2022/2554 of the European Parliament and of the Council with regard to regulatory technical standards specifying ICT risk management tools, methods, processes, and policies and the simplified ICT risk management framework. Official Journal of the European Union. Available at: <eur-lex.europa.eu> [Accessed 22 October 2026]. European Union, 2022. Regulation (EU) 2022/2554 of the European Parliament and of the Council of 14 December 2022 on digital operational resilience for the financial sector and amending Regulations (EC) No 1060/2009, (EU) No 648/2012, (EU) No 600/2014, (EU) No 909/2014 and (EU) 2016/1011. Official Journal of the European Union, L 333. Available at: <eur-lex.europa.eu> [Accessed 22 October 2026]. Google Cloud, n.d.a. Analyze allow policies. Google Cloud Documentation. Available at: <docs.cloud.google.com> [Accessed 15 October 2026]. Google Cloud, n.d.b. Asset types. Google Cloud Documentation. Available at: <docs.cloud.google.com> [Accessed 15 October 2026]. Google Cloud, n.d.c. AuditLog. Google Cloud Documentation. Available at: <docs.cloud.google.com> [Accessed 22 October 2026]. Google Cloud, n.d.d. Batch and route log entries retroactively. Google Cloud Documentation. Available at: <docs.cloud.google.com> [Accessed 22 October 2026]. Google Cloud, n.d.e. Best practices for managing service account keys. Google Cloud Documentation. Available at: <docs.cloud.google.com> [Accessed 15 October 2026]. Google Cloud, n.d.f. BigQuery audit logs overview. Google Cloud Documentation. Available at: <docs.cloud.google.com> [Accessed 15 October 2026]. Google Cloud, n.d.g. BigQuery IAM roles and permissions. Google Cloud Documentation. Available at: <docs.cloud.google.com> [Accessed 15 October 2026]. Google Cloud, n.d.h. Cloud Asset Inventory roles and permissions. Google Cloud Documentation. Available at: <docs.cloud.google.com> [Accessed 22 October 2026]. Google Cloud, n.d.i. Cloud Audit Logs overview. Google Cloud Documentation. Available at: <docs.cloud.google.com> [Accessed 11 October 2026]. Google Cloud, n.d.j. Cloud Logging quotas and limits. Google Cloud Documentation. Available at: <docs.cloud.google.com> [Accessed 11 October 2026]. Google Cloud, n.d.k. Configure log buckets. Google Cloud Documentation. Available at: <docs.cloud.google.com> [Accessed 11 October 2026]. Google Cloud, n.d.l. Configure Workload Identity Federation with AWS or Azure VMs. Google Cloud Documentation. Available at: <docs.cloud.google.com> [Accessed 15 October 2026]. Google Cloud, n.d.m. Configure Workload Identity Federation with deployment pipelines. Google Cloud Documentation. Available at: <docs.cloud.google.com> [Accessed 22 October 2026]. Google Cloud, n.d.n. Disable and enable service account keys. Google Cloud Documentation. Available at: <docs.cloud.google.com> [Accessed 15

Latest stargazers

The last 10 distinct accounts we observed this year.

Most active participants

PR, issue, comment & review events in the past year.

Packages

11
PackageVersionDownloads · 30dTrend · 30d
———
v1.24.5
≥22.4M
dbt-athenav1.11.1
v1.11.1
≥1.3M
v1.11.1
≥571.1K
v1.12.1
≥4.1M
dbt-lakecomputev0.1.0.dev2
v0.1.0.dev2
≥175
v1.2.7
≥4.9M
v1.11.1
≥2M
v1.12.4
≥5.7M
dbt-sparkv1.11.0
v1.11.0
≥4.2M
v1.2.7
≥28.1K

README badges

Embed a badge in your README or website.

Live mentions · last 30 days
dbt-labs/dbt-adapters mentions in the last 30 days