[{"data":1,"prerenderedAt":1342},["ShallowReactive",2],{"blog-how-to/fix-firebase-api-key-exposure":3},{"id":4,"title":5,"body":6,"category":1312,"date":1313,"dateModified":1313,"description":1314,"draft":1315,"extension":1316,"faq":1317,"featured":1315,"headerVariant":1327,"image":1328,"keywords":1329,"meta":1330,"navigation":318,"ogDescription":1331,"ogTitle":1328,"path":1332,"readTime":1333,"schemaOrg":1334,"schemaType":1335,"seo":1336,"sitemap":1337,"stem":1338,"tags":1339,"twitterCard":1340,"__hash__":1341},"blog/blog/how-to/fix-firebase-api-key-exposure.md","How to Fix Firebase API Key Exposure (2026)",{"type":7,"value":8,"toc":1300},"minimark",[9,13,16,35,40,43,48,51,110,120,127,131,134,232,235,241,251,255,357,443,466,472,476,479,490,502,518,527,536,553,557,560,657,664,727,731,737,746,749,757,760,766,769,775,792,798,802,805,810,849,854,1015,1018,1046,1056,1072,1076,1081,1199,1205,1215,1270,1289,1296],[10,11,12],"p",{},"A Firebase service account JSON file committed to a public GitHub repo gives any attacker full admin access to your Firestore database, the ability to create and delete users, and a path to your Cloud Storage buckets. We find this credential type in source repos more often than any other Firebase issue.",[10,14,15],{},"But there's a complication: Firebase has two completely different types of credentials, and most \"Firebase API key exposure\" guides confuse them. Fixing the wrong one wastes time and leaves the real risk open.",[17,18,19],"tldr",{},[10,20,21,22,26,27,30,31,34],{},"The ",[23,24,25],"code",{},"apiKey"," in your Firebase web SDK config (",[23,28,29],{},"AIzaSy...",") is not a secret. It's a project identifier that Google publishes intentionally. Your data is protected by Security Rules, not by keeping this key private. What you must protect is the Firebase Admin SDK service account JSON file. If that file is in your git history or in client-side JavaScript, rotate it immediately. Also check that your Security Rules are not set to ",[23,32,33],{},"allow read, write: if true",".",[36,37,39],"h2",{"id":38},"two-credentials-two-threat-models","Two Credentials, Two Threat Models",[10,41,42],{},"Before rotating anything, identify which one leaked.",[44,45,47],"h3",{"id":46},"the-public-web-api-key","The Public Web API Key",[10,49,50],{},"Your Firebase project has a config object that goes in your frontend code:",[52,53,58],"pre",{"className":54,"code":55,"language":56,"meta":57,"style":57},"language-javascript shiki shiki-themes github-light github-dark","const firebaseConfig = {\n  apiKey: \"AIzaSyD-9tSrke72I6e4\",\n  authDomain: \"your-project.firebaseapp.com\",\n  projectId: \"your-project\",\n  storageBucket: \"your-project.appspot.com\",\n  messagingSenderId: \"123456789\",\n  appId: \"1:123456789:web:abc123\"\n};\n","javascript","",[23,59,60,68,74,80,86,92,98,104],{"__ignoreMap":57},[61,62,65],"span",{"class":63,"line":64},"line",1,[61,66,67],{},"const firebaseConfig = {\n",[61,69,71],{"class":63,"line":70},2,[61,72,73],{},"  apiKey: \"AIzaSyD-9tSrke72I6e4\",\n",[61,75,77],{"class":63,"line":76},3,[61,78,79],{},"  authDomain: \"your-project.firebaseapp.com\",\n",[61,81,83],{"class":63,"line":82},4,[61,84,85],{},"  projectId: \"your-project\",\n",[61,87,89],{"class":63,"line":88},5,[61,90,91],{},"  storageBucket: \"your-project.appspot.com\",\n",[61,93,95],{"class":63,"line":94},6,[61,96,97],{},"  messagingSenderId: \"123456789\",\n",[61,99,101],{"class":63,"line":100},7,[61,102,103],{},"  appId: \"1:123456789:web:abc123\"\n",[61,105,107],{"class":63,"line":106},8,[61,108,109],{},"};\n",[10,111,112,113,115,116,119],{},"That ",[23,114,25],{}," starting with ",[23,117,118],{},"AIzaSy"," is visible in your deployed JavaScript and that's intentional. Google's documentation explicitly says this value is not secret. It tells the Firebase SDK which project to connect to. Your Security Rules determine whether any given operation succeeds. An attacker with only this key can read your database if and only if your rules permit unauthenticated reads.",[10,121,122,126],{},[123,124,125],"strong",{},"If only the web apiKey is \"exposed\":"," You don't need to rotate it. Fix your Security Rules instead (see Step 4 below).",[44,128,130],{"id":129},"the-admin-sdk-service-account-key","The Admin SDK Service Account Key",[10,132,133],{},"This is the dangerous one. It looks like a JSON file:",[52,135,139],{"className":136,"code":137,"language":138,"meta":57,"style":57},"language-json shiki shiki-themes github-light github-dark","{\n  \"type\": \"service_account\",\n  \"project_id\": \"your-project\",\n  \"private_key_id\": \"abc123\",\n  \"private_key\": \"-----BEGIN RSA PRIVATE KEY-----\\nMIIEowI...\",\n  \"client_email\": \"firebase-adminsdk-xyz@your-project.iam.gserviceaccount.com\",\n  \"client_id\": \"12345678901234567890\"\n}\n","json",[23,140,141,147,163,175,187,205,217,227],{"__ignoreMap":57},[61,142,143],{"class":63,"line":64},[61,144,146],{"class":145},"sVt8B","{\n",[61,148,149,153,156,160],{"class":63,"line":70},[61,150,152],{"class":151},"sj4cs","  \"type\"",[61,154,155],{"class":145},": ",[61,157,159],{"class":158},"sZZnC","\"service_account\"",[61,161,162],{"class":145},",\n",[61,164,165,168,170,173],{"class":63,"line":76},[61,166,167],{"class":151},"  \"project_id\"",[61,169,155],{"class":145},[61,171,172],{"class":158},"\"your-project\"",[61,174,162],{"class":145},[61,176,177,180,182,185],{"class":63,"line":82},[61,178,179],{"class":151},"  \"private_key_id\"",[61,181,155],{"class":145},[61,183,184],{"class":158},"\"abc123\"",[61,186,162],{"class":145},[61,188,189,192,194,197,200,203],{"class":63,"line":88},[61,190,191],{"class":151},"  \"private_key\"",[61,193,155],{"class":145},[61,195,196],{"class":158},"\"-----BEGIN RSA PRIVATE KEY-----",[61,198,199],{"class":151},"\\n",[61,201,202],{"class":158},"MIIEowI...\"",[61,204,162],{"class":145},[61,206,207,210,212,215],{"class":63,"line":94},[61,208,209],{"class":151},"  \"client_email\"",[61,211,155],{"class":145},[61,213,214],{"class":158},"\"firebase-adminsdk-xyz@your-project.iam.gserviceaccount.com\"",[61,216,162],{"class":145},[61,218,219,222,224],{"class":63,"line":100},[61,220,221],{"class":151},"  \"client_id\"",[61,223,155],{"class":145},[61,225,226],{"class":158},"\"12345678901234567890\"\n",[61,228,229],{"class":63,"line":106},[61,230,231],{"class":145},"}\n",[10,233,234],{},"This key bypasses every Security Rule. Anyone who has it can read, write, and delete everything in Firestore, Realtime Database, and Cloud Storage without authentication. It's also the credential that lets you create and delete Firebase Auth users.",[10,236,237,240],{},[123,238,239],{},"If this file is in your git history or client-side code:"," Rotate immediately. This guide's Steps 2-3 cover that.",[242,243,244],"danger-box",{},[10,245,246,247,250],{},"Never import ",[23,248,249],{},"firebase-admin"," in your React, Vue, or Vite app. The Admin SDK requires the service account credentials, and any import in client-side code will bundle those credentials into your JavaScript. Every visitor to your site can then extract them.",[36,252,254],{"id":253},"step-1-find-what-actually-leaked","Step 1: Find What Actually Leaked",[256,257,259,264,354],"step",{"number":258},"1",[10,260,261],{},[123,262,263],{},"Check git history for the service account JSON file.",[52,265,269],{"className":266,"code":267,"language":268,"meta":57,"style":57},"language-bash shiki shiki-themes github-light github-dark","# Look for JSON files with service_account content ever committed\ngit log --all --full-history --name-only -- \"*.json\" | grep -E \"\\-adminsdk-|service-account|firebase-credentials\"\n\n# Search commit contents directly\ngit log --all -p | grep -A5 '\"type\": \"service_account\"' | head -20\n","bash",[23,270,271,277,314,320,325],{"__ignoreMap":57},[61,272,273],{"class":63,"line":64},[61,274,276],{"class":275},"sJ8bj","# Look for JSON files with service_account content ever committed\n",[61,278,279,283,286,289,292,295,298,301,305,308,311],{"class":63,"line":70},[61,280,282],{"class":281},"sScJk","git",[61,284,285],{"class":158}," log",[61,287,288],{"class":151}," --all",[61,290,291],{"class":151}," --full-history",[61,293,294],{"class":151}," --name-only",[61,296,297],{"class":151}," --",[61,299,300],{"class":158}," \"*.json\"",[61,302,304],{"class":303},"szBVR"," |",[61,306,307],{"class":281}," grep",[61,309,310],{"class":151}," -E",[61,312,313],{"class":158}," \"\\-adminsdk-|service-account|firebase-credentials\"\n",[61,315,316],{"class":63,"line":76},[61,317,319],{"emptyLinePlaceholder":318},true,"\n",[61,321,322],{"class":63,"line":82},[61,323,324],{"class":275},"# Search commit contents directly\n",[61,326,327,329,331,333,336,338,340,343,346,348,351],{"class":63,"line":88},[61,328,282],{"class":281},[61,330,285],{"class":158},[61,332,288],{"class":151},[61,334,335],{"class":151}," -p",[61,337,304],{"class":303},[61,339,307],{"class":281},[61,341,342],{"class":151}," -A5",[61,344,345],{"class":158}," '\"type\": \"service_account\"'",[61,347,304],{"class":303},[61,349,350],{"class":281}," head",[61,352,353],{"class":151}," -20\n",[10,355,356],{},"If any commits return results, the key was in your repository and should be considered compromised.",[256,358,360,365,429],{"number":359},"2",[10,361,362],{},[123,363,364],{},"Search your current codebase for Admin SDK credentials.",[52,366,368],{"className":266,"code":367,"language":268,"meta":57,"style":57},"# Find imported service account files\ngrep -r \"require.*firebase.*json\\|import.*serviceAccount\" --include=\"*.js\" --include=\"*.ts\" .\n\n# Find environment variables that might hold the private key inline\ngrep -r \"FIREBASE_ADMIN_PRIVATE_KEY\\|FIREBASE_SERVICE_ACCOUNT\" --include=\"*.env*\" --include=\"*.ts\" .\n",[23,369,370,375,400,404,409],{"__ignoreMap":57},[61,371,372],{"class":63,"line":64},[61,373,374],{"class":275},"# Find imported service account files\n",[61,376,377,380,383,386,389,392,394,397],{"class":63,"line":70},[61,378,379],{"class":281},"grep",[61,381,382],{"class":151}," -r",[61,384,385],{"class":158}," \"require.*firebase.*json\\|import.*serviceAccount\"",[61,387,388],{"class":151}," --include=",[61,390,391],{"class":158},"\"*.js\"",[61,393,388],{"class":151},[61,395,396],{"class":158},"\"*.ts\"",[61,398,399],{"class":158}," .\n",[61,401,402],{"class":63,"line":76},[61,403,319],{"emptyLinePlaceholder":318},[61,405,406],{"class":63,"line":82},[61,407,408],{"class":275},"# Find environment variables that might hold the private key inline\n",[61,410,411,413,415,418,420,423,425,427],{"class":63,"line":88},[61,412,379],{"class":281},[61,414,382],{"class":151},[61,416,417],{"class":158}," \"FIREBASE_ADMIN_PRIVATE_KEY\\|FIREBASE_SERVICE_ACCOUNT\"",[61,419,388],{"class":151},[61,421,422],{"class":158},"\"*.env*\"",[61,424,388],{"class":151},[61,426,396],{"class":158},[61,428,399],{"class":158},[10,430,431,432,435,436,435,439,442],{},"If any of these appear in files under ",[23,433,434],{},"src/",", ",[23,437,438],{},"app/",[23,440,441],{},"components/",", or similar client directories, the credential is in your bundle.",[256,444,446,451],{"number":445},"3",[10,447,448],{},[123,449,450],{},"Check your deployed JavaScript bundle.",[10,452,453,454,457,458,461,462,465],{},"Open your live app in a browser, press F12, go to the Sources tab, and search (Ctrl+F) for ",[23,455,456],{},"private_key_id"," or ",[23,459,460],{},"iam.gserviceaccount.com",". Finding either string in a ",[23,463,464],{},".js"," file means the service account is bundled client-side.",[467,468,469],"tip-box",{},[10,470,471],{},"CheckYourVibe scans your deployed app for Firebase service account keys in the JavaScript bundle. It also checks whether your Firestore rules allow unauthenticated access, which is a separate but equally serious issue.",[36,473,475],{"id":474},"step-2-rotate-the-service-account-key","Step 2: Rotate the Service Account Key",[10,477,478],{},"If the service account JSON was exposed anywhere, rotate it before doing anything else.",[256,480,481],{"number":258},[10,482,483,486,487,34],{},[123,484,485],{},"Open the Google Cloud Console."," Go to console.cloud.google.com, select your project, and navigate to ",[123,488,489],{},"IAM & Admin > Service Accounts",[256,491,492],{"number":359},[10,493,494,497,498,501],{},[123,495,496],{},"Find the Firebase Admin SDK service account."," It has an email ending in ",[23,499,500],{},"firebase-adminsdk-XXXXX@your-project-id.iam.gserviceaccount.com",". Click it.",[256,503,504],{"number":445},[10,505,506,509,510,513,514,517],{},[123,507,508],{},"Add a new key first."," Click the ",[123,511,512],{},"Keys"," tab, then ",[123,515,516],{},"Add Key > Create new key",", choose JSON, and download the file. Store it somewhere secure (not your repo).",[256,519,521],{"number":520},"4",[10,522,523,526],{},[123,524,525],{},"Update your server environment with the new key."," Paste the new credentials into your server's environment variables (see Step 5 for the correct format). Redeploy.",[256,528,530],{"number":529},"5",[10,531,532,535],{},[123,533,534],{},"Delete the old key."," Once your server is running with the new key, go back to the Keys tab and delete the compromised key. It is immediately invalidated.",[256,537,539],{"number":538},"6",[10,540,541,544,545,548,549,552],{},[123,542,543],{},"Review Cloud Audit Logs."," In Google Cloud Console, go to ",[123,546,547],{},"Logging > Logs Explorer"," and filter by the old ",[23,550,551],{},"client_email"," to see if unauthorized operations were performed during the exposure window.",[36,554,556],{"id":555},"step-3-remove-from-git-history-if-committed","Step 3: Remove from Git History (if committed)",[10,558,559],{},"If your audit found the file in git history, the key is compromised even after deletion because anyone who cloned the repo before you scrubbed it may still have a copy. Rotate first, then clean.",[52,561,563],{"className":266,"code":562,"language":268,"meta":57,"style":57},"# Install git-filter-repo (preferred tool)\npip install git-filter-repo\n\n# Remove the service account file from all history\ngit filter-repo --path firebase-adminsdk.json --invert-paths\ngit filter-repo --path serviceAccountKey.json --invert-paths\ngit filter-repo --path firebase-credentials.json --invert-paths\n\n# If you're unsure of the filename, use regex\ngit filter-repo --paths-glob \"**/*service*account*.json\" --invert-paths\n",[23,564,565,570,581,585,590,606,619,632,636,642],{"__ignoreMap":57},[61,566,567],{"class":63,"line":64},[61,568,569],{"class":275},"# Install git-filter-repo (preferred tool)\n",[61,571,572,575,578],{"class":63,"line":70},[61,573,574],{"class":281},"pip",[61,576,577],{"class":158}," install",[61,579,580],{"class":158}," git-filter-repo\n",[61,582,583],{"class":63,"line":76},[61,584,319],{"emptyLinePlaceholder":318},[61,586,587],{"class":63,"line":82},[61,588,589],{"class":275},"# Remove the service account file from all history\n",[61,591,592,594,597,600,603],{"class":63,"line":88},[61,593,282],{"class":281},[61,595,596],{"class":158}," filter-repo",[61,598,599],{"class":151}," --path",[61,601,602],{"class":158}," firebase-adminsdk.json",[61,604,605],{"class":151}," --invert-paths\n",[61,607,608,610,612,614,617],{"class":63,"line":94},[61,609,282],{"class":281},[61,611,596],{"class":158},[61,613,599],{"class":151},[61,615,616],{"class":158}," serviceAccountKey.json",[61,618,605],{"class":151},[61,620,621,623,625,627,630],{"class":63,"line":100},[61,622,282],{"class":281},[61,624,596],{"class":158},[61,626,599],{"class":151},[61,628,629],{"class":158}," firebase-credentials.json",[61,631,605],{"class":151},[61,633,634],{"class":63,"line":106},[61,635,319],{"emptyLinePlaceholder":318},[61,637,639],{"class":63,"line":638},9,[61,640,641],{"class":275},"# If you're unsure of the filename, use regex\n",[61,643,645,647,649,652,655],{"class":63,"line":644},10,[61,646,282],{"class":281},[61,648,596],{"class":158},[61,650,651],{"class":151}," --paths-glob",[61,653,654],{"class":158}," \"**/*service*account*.json\"",[61,656,605],{"class":151},[10,658,659,660,663],{},"After scrubbing, force-push to all remotes and ask any collaborators to re-clone. Then add the file pattern to ",[23,661,662],{},".gitignore",":",[52,665,667],{"className":266,"code":666,"language":268,"meta":57,"style":57},"echo \"*service*account*.json\" >> .gitignore\necho \"firebase-adminsdk*.json\" >> .gitignore\necho \"*serviceAccountKey*.json\" >> .gitignore\ngit add .gitignore\ngit commit -m \"chore: gitignore firebase service account files\"\n",[23,668,669,683,694,705,714],{"__ignoreMap":57},[61,670,671,674,677,680],{"class":63,"line":64},[61,672,673],{"class":151},"echo",[61,675,676],{"class":158}," \"*service*account*.json\"",[61,678,679],{"class":303}," >>",[61,681,682],{"class":158}," .gitignore\n",[61,684,685,687,690,692],{"class":63,"line":70},[61,686,673],{"class":151},[61,688,689],{"class":158}," \"firebase-adminsdk*.json\"",[61,691,679],{"class":303},[61,693,682],{"class":158},[61,695,696,698,701,703],{"class":63,"line":76},[61,697,673],{"class":151},[61,699,700],{"class":158}," \"*serviceAccountKey*.json\"",[61,702,679],{"class":303},[61,704,682],{"class":158},[61,706,707,709,712],{"class":63,"line":82},[61,708,282],{"class":281},[61,710,711],{"class":158}," add",[61,713,682],{"class":158},[61,715,716,718,721,724],{"class":63,"line":88},[61,717,282],{"class":281},[61,719,720],{"class":158}," commit",[61,722,723],{"class":151}," -m",[61,725,726],{"class":158}," \"chore: gitignore firebase service account files\"\n",[36,728,730],{"id":729},"step-4-fix-open-security-rules","Step 4: Fix Open Security Rules",[10,732,733,734,736],{},"Even if no Admin SDK key leaked, open Security Rules leave your entire database readable by anyone who knows your project ID (which is public). In 2022, Cybernews researchers scanned Firebase projects and found more than 900 apps running Firestore or Realtime Database with ",[23,735,33],{},", requiring no authentication at all.",[10,738,739,742,743,34],{},[123,740,741],{},"Check your current Firestore rules"," in the Firebase Console under ",[123,744,745],{},"Firestore Database > Rules",[10,747,748],{},"Insecure (open):",[52,750,755],{"className":751,"code":753,"language":754},[752],"language-text","rules_version = '2';\nservice cloud.firestore {\n  match /databases/{database}/documents {\n    match /{document=**} {\n      allow read, write: if true;  // anyone can do anything\n    }\n  }\n}\n","text",[23,756,753],{"__ignoreMap":57},[10,758,759],{},"Minimum secure baseline:",[52,761,764],{"className":762,"code":763,"language":754},[752],"rules_version = '2';\nservice cloud.firestore {\n  match /databases/{database}/documents {\n    match /{document=**} {\n      allow read, write: if request.auth != null;  // must be logged in\n    }\n  }\n}\n",[23,765,763],{"__ignoreMap":57},[10,767,768],{},"For user-specific data, scope it further:",[52,770,773],{"className":771,"code":772,"language":754},[752],"match /users/{userId}/{document=**} {\n  allow read, write: if request.auth != null && request.auth.uid == userId;\n}\n",[23,774,772],{"__ignoreMap":57},[10,776,777,778,781,782,785,786,781,789,34],{},"Apply the same logic to ",[123,779,780],{},"Realtime Database"," under ",[123,783,784],{},"Realtime Database > Rules"," and to ",[123,787,788],{},"Storage",[123,790,791],{},"Storage > Rules",[793,794,795],"warning-box",{},[10,796,797],{},"The Firebase Console sends you an email if your rules allow unauthenticated access, but many founders dismiss it as noise. That email is a real alert. The default test-mode rules that Firebase creates when you set up a new project have a 30-day expiry; after that they lock down automatically. If you extended them or copied them without the expiry, check now.",[36,799,801],{"id":800},"step-5-move-the-admin-sdk-server-side","Step 5: Move the Admin SDK Server-Side",[10,803,804],{},"The Admin SDK should only ever run in Node.js server code. Here's the correct pattern using environment variables instead of a JSON file:",[10,806,807],{},[123,808,809],{},"Wrong (Admin SDK in client-side code):",[52,811,813],{"className":54,"code":812,"language":56,"meta":57,"style":57},"// src/lib/firebase.ts (client-side code, do not use Admin SDK here)\nimport admin from 'firebase-admin';\nimport serviceAccount from './firebase-adminsdk.json'; // never do this\n\nadmin.initializeApp({\n  credential: admin.credential.cert(serviceAccount)\n});\n",[23,814,815,820,825,830,834,839,844],{"__ignoreMap":57},[61,816,817],{"class":63,"line":64},[61,818,819],{},"// src/lib/firebase.ts (client-side code, do not use Admin SDK here)\n",[61,821,822],{"class":63,"line":70},[61,823,824],{},"import admin from 'firebase-admin';\n",[61,826,827],{"class":63,"line":76},[61,828,829],{},"import serviceAccount from './firebase-adminsdk.json'; // never do this\n",[61,831,832],{"class":63,"line":82},[61,833,319],{"emptyLinePlaceholder":318},[61,835,836],{"class":63,"line":88},[61,837,838],{},"admin.initializeApp({\n",[61,840,841],{"class":63,"line":94},[61,842,843],{},"  credential: admin.credential.cert(serviceAccount)\n",[61,845,846],{"class":63,"line":100},[61,847,848],{},"});\n",[10,850,851],{},[123,852,853],{},"Right (Admin SDK in server-side API route):",[52,855,859],{"className":856,"code":857,"language":858,"meta":57,"style":57},"language-typescript shiki shiki-themes github-light github-dark","// server/api/admin-action.post.ts (Nuxt) or app/api/admin-action/route.ts (Next.js)\nimport admin from 'firebase-admin';\n\nif (!admin.apps.length) {\n  admin.initializeApp({\n    credential: admin.credential.cert({\n      projectId: process.env.FIREBASE_PROJECT_ID,\n      clientEmail: process.env.FIREBASE_CLIENT_EMAIL,\n      // Replace escaped newlines in the private key\n      privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\\\n/g, '\\n'),\n    }),\n  });\n}\n","typescript",[23,860,861,866,883,887,907,918,928,938,948,953,998,1004,1010],{"__ignoreMap":57},[61,862,863],{"class":63,"line":64},[61,864,865],{"class":275},"// server/api/admin-action.post.ts (Nuxt) or app/api/admin-action/route.ts (Next.js)\n",[61,867,868,871,874,877,880],{"class":63,"line":70},[61,869,870],{"class":303},"import",[61,872,873],{"class":145}," admin ",[61,875,876],{"class":303},"from",[61,878,879],{"class":158}," 'firebase-admin'",[61,881,882],{"class":145},";\n",[61,884,885],{"class":63,"line":76},[61,886,319],{"emptyLinePlaceholder":318},[61,888,889,892,895,898,901,904],{"class":63,"line":82},[61,890,891],{"class":303},"if",[61,893,894],{"class":145}," (",[61,896,897],{"class":303},"!",[61,899,900],{"class":145},"admin.apps.",[61,902,903],{"class":151},"length",[61,905,906],{"class":145},") {\n",[61,908,909,912,915],{"class":63,"line":88},[61,910,911],{"class":145},"  admin.",[61,913,914],{"class":281},"initializeApp",[61,916,917],{"class":145},"({\n",[61,919,920,923,926],{"class":63,"line":94},[61,921,922],{"class":145},"    credential: admin.credential.",[61,924,925],{"class":281},"cert",[61,927,917],{"class":145},[61,929,930,933,936],{"class":63,"line":100},[61,931,932],{"class":145},"      projectId: process.env.",[61,934,935],{"class":151},"FIREBASE_PROJECT_ID",[61,937,162],{"class":145},[61,939,940,943,946],{"class":63,"line":106},[61,941,942],{"class":145},"      clientEmail: process.env.",[61,944,945],{"class":151},"FIREBASE_CLIENT_EMAIL",[61,947,162],{"class":145},[61,949,950],{"class":63,"line":638},[61,951,952],{"class":275},"      // Replace escaped newlines in the private key\n",[61,954,955,958,961,964,967,970,973,977,981,983,986,988,991,993,995],{"class":63,"line":644},[61,956,957],{"class":145},"      privateKey: process.env.",[61,959,960],{"class":151},"FIREBASE_PRIVATE_KEY",[61,962,963],{"class":145},"?.",[61,965,966],{"class":281},"replace",[61,968,969],{"class":145},"(",[61,971,972],{"class":158},"/",[61,974,976],{"class":975},"snhLl","\\\\",[61,978,980],{"class":979},"sA_wV","n",[61,982,972],{"class":158},[61,984,985],{"class":303},"g",[61,987,435],{"class":145},[61,989,990],{"class":158},"'",[61,992,199],{"class":151},[61,994,990],{"class":158},[61,996,997],{"class":145},"),\n",[61,999,1001],{"class":63,"line":1000},11,[61,1002,1003],{"class":145},"    }),\n",[61,1005,1007],{"class":63,"line":1006},12,[61,1008,1009],{"class":145},"  });\n",[61,1011,1013],{"class":63,"line":1012},13,[61,1014,231],{"class":145},[10,1016,1017],{},"Set these three environment variables on your server:",[1019,1020,1021,1031,1038],"ul",{},[1022,1023,1024,1026,1027,1030],"li",{},[23,1025,935],{},": the ",[23,1028,1029],{},"project_id"," value from the JSON",[1022,1032,1033,1026,1035,1037],{},[23,1034,945],{},[23,1036,551],{}," value",[1022,1039,1040,1026,1042,1045],{},[23,1041,960],{},[23,1043,1044],{},"private_key"," value (the full RSA key string)",[10,1047,1048,1049,1051,1052,1055],{},"The private key contains literal ",[23,1050,199],{}," characters when stored as an environment variable. That's why the ",[23,1053,1054],{},".replace(/\\\\n/g, '\\n')"," call is necessary.",[467,1057,1058],{},[10,1059,1060,1061,1063,1064,1067,1068,1071],{},"When copying the ",[23,1062,1044],{}," value into an environment variable, copy the entire string including the ",[23,1065,1066],{},"-----BEGIN RSA PRIVATE KEY-----"," and ",[23,1069,1070],{},"-----END RSA PRIVATE KEY-----"," headers. Most platforms (Vercel, Railway, Render) handle multiline values fine just paste the raw string.",[36,1073,1075],{"id":1074},"step-6-prevent-future-leaks","Step 6: Prevent Future Leaks",[10,1077,1078],{},[123,1079,1080],{},"Gitleaks pre-commit hook:",[52,1082,1084],{"className":266,"code":1083,"language":268,"meta":57,"style":57},"brew install gitleaks  # or: go install github.com/zricethezav/gitleaks/v8@latest\n\n# Test your repo now\ngitleaks detect --source . --verbose\n\n# Add as pre-commit hook\ncat > .git/hooks/pre-commit \u003C\u003C 'EOF'\n#!/bin/sh\ngitleaks protect --staged --verbose\nif [ $? -ne 0 ]; then\n  echo \"Gitleaks found potential secrets. Commit blocked.\"\n  exit 1\nfi\nEOF\nchmod +x .git/hooks/pre-commit\n",[23,1085,1086,1099,1103,1108,1125,1129,1134,1151,1156,1161,1166,1171,1176,1181,1187],{"__ignoreMap":57},[61,1087,1088,1091,1093,1096],{"class":63,"line":64},[61,1089,1090],{"class":281},"brew",[61,1092,577],{"class":158},[61,1094,1095],{"class":158}," gitleaks",[61,1097,1098],{"class":275},"  # or: go install github.com/zricethezav/gitleaks/v8@latest\n",[61,1100,1101],{"class":63,"line":70},[61,1102,319],{"emptyLinePlaceholder":318},[61,1104,1105],{"class":63,"line":76},[61,1106,1107],{"class":275},"# Test your repo now\n",[61,1109,1110,1113,1116,1119,1122],{"class":63,"line":82},[61,1111,1112],{"class":281},"gitleaks",[61,1114,1115],{"class":158}," detect",[61,1117,1118],{"class":151}," --source",[61,1120,1121],{"class":158}," .",[61,1123,1124],{"class":151}," --verbose\n",[61,1126,1127],{"class":63,"line":88},[61,1128,319],{"emptyLinePlaceholder":318},[61,1130,1131],{"class":63,"line":94},[61,1132,1133],{"class":275},"# Add as pre-commit hook\n",[61,1135,1136,1139,1142,1145,1148],{"class":63,"line":100},[61,1137,1138],{"class":281},"cat",[61,1140,1141],{"class":303}," >",[61,1143,1144],{"class":158}," .git/hooks/pre-commit",[61,1146,1147],{"class":303}," \u003C\u003C",[61,1149,1150],{"class":158}," 'EOF'\n",[61,1152,1153],{"class":63,"line":106},[61,1154,1155],{"class":158},"#!/bin/sh\n",[61,1157,1158],{"class":63,"line":638},[61,1159,1160],{"class":158},"gitleaks protect --staged --verbose\n",[61,1162,1163],{"class":63,"line":644},[61,1164,1165],{"class":158},"if [ $? -ne 0 ]; then\n",[61,1167,1168],{"class":63,"line":1000},[61,1169,1170],{"class":158},"  echo \"Gitleaks found potential secrets. Commit blocked.\"\n",[61,1172,1173],{"class":63,"line":1006},[61,1174,1175],{"class":158},"  exit 1\n",[61,1177,1178],{"class":63,"line":1012},[61,1179,1180],{"class":158},"fi\n",[61,1182,1184],{"class":63,"line":1183},14,[61,1185,1186],{"class":158},"EOF\n",[61,1188,1190,1193,1196],{"class":63,"line":1189},15,[61,1191,1192],{"class":281},"chmod",[61,1194,1195],{"class":158}," +x",[61,1197,1198],{"class":158}," .git/hooks/pre-commit\n",[10,1200,1201,1202,34],{},"Gitleaks has built-in detection patterns for Firebase service account JSON files. It will block any commit that includes a file containing ",[23,1203,1204],{},"\"type\": \"service_account\"",[10,1206,1207,1210,1211,1214],{},[123,1208,1209],{},"Firebase App Check"," adds a second layer by restricting which apps (identified by platform attestation) can use your project's APIs. Enable it in Firebase Console under ",[123,1212,1213],{},"App Check",". It does not replace Security Rules, but it reduces automated scanning of your endpoints.",[1216,1217,1218,1229,1249,1258,1264],"faq-section",{},[1219,1220,1222],"faq-item",{"question":1221},"Is a Firebase API key secret?",[10,1223,21,1224,26,1226,1228],{},[23,1225,25],{},[23,1227,29],{},") is not a secret. Google intentionally makes it public. It just identifies your project so the SDK can connect. What's secret is your Firebase Admin SDK service account JSON key, which bypasses all Security Rules. Never commit the JSON file or use it in client-side code.",[1219,1230,1232],{"question":1231},"How do I know if my Firebase service account key is exposed?",[10,1233,1234,1235,1238,1239,457,1241,1243,1244,457,1246,1248],{},"Search git history with ",[23,1236,1237],{},"git log --all -p | grep '\"type\": \"service_account\"'",". Check your deployed JavaScript bundle in browser DevTools for ",[23,1240,456],{},[23,1242,460],{}," strings. If your Admin SDK import appears in client-side code under ",[23,1245,434],{},[23,1247,438],{},", the credentials are reachable from the browser.",[1219,1250,1252],{"question":1251},"What happens if Firebase Security Rules are set to allow read, write: if true?",[10,1253,1254,1255,34],{},"Any person on the internet with your project ID (which is public) can read and overwrite your entire Firestore or Realtime Database without logging in. In 2022, Cybernews researchers found more than 900 Firebase apps in this state. Replace open rules with at minimum: ",[23,1256,1257],{},"allow read, write: if request.auth != null",[1219,1259,1261],{"question":1260},"Can I use the Firebase Admin SDK in a Vite or React app?",[10,1262,1263],{},"No. The Admin SDK is a Node.js library that requires your service account credentials and bypasses all Security Rules. Running it in a browser would expose those credentials to every visitor. Use the Admin SDK only in server-side code: a Cloud Function, an Express server, or a Nuxt/Next.js API route.",[1219,1265,1267],{"question":1266},"How do I rotate a Firebase service account key without downtime?",[10,1268,1269],{},"In Google Cloud Console, go to IAM & Admin > Service Accounts, click the Firebase Admin SDK account, open the Keys tab, and add a new JSON key first. Update your server environment variable with the new credentials and redeploy. Once the new key is confirmed working, delete the old key. This rotation keeps production running throughout.",[1271,1272,1273,1279,1284],"related-articles",{},[1274,1275],"related-card",{"description":1276,"href":1277,"title":1278},"Complete guide to Firestore and Realtime Database security rules. Learn rule syntax, common patterns, testing, and debugging.","/blog/how-to/firebase-security-rules","How to Write Firebase Security Rules",[1274,1280],{"description":1281,"href":1282,"title":1283},"Clean secrets from your git history after accidental commits using git filter-repo and BFG Repo Cleaner.","/blog/how-to/remove-secrets-git-history","How to Remove Secrets from Git History",[1274,1285],{"description":1286,"href":1287,"title":1288},"Emergency guide for rotating compromised API keys without downtime. Step-by-step for Stripe, OpenAI, Supabase, and others.","/blog/how-to/rotate-api-keys","How to Rotate API Keys",[1290,1291,1293],"cta-box",{"href":972,"label":1292},"Scan Your Firebase App",[10,1294,1295],{},"Check your Firebase deployment for service account keys in your JavaScript bundle, open Security Rules, and missing App Check configuration. Free scan, no signup required.",[1297,1298,1299],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .snhLl, html code.shiki .snhLl{--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold}html pre.shiki code .sA_wV, html code.shiki .sA_wV{--shiki-default:#032F62;--shiki-dark:#DBEDFF}",{"title":57,"searchDepth":70,"depth":70,"links":1301},[1302,1306,1307,1308,1309,1310,1311],{"id":38,"depth":70,"text":39,"children":1303},[1304,1305],{"id":46,"depth":76,"text":47},{"id":129,"depth":76,"text":130},{"id":253,"depth":70,"text":254},{"id":474,"depth":70,"text":475},{"id":555,"depth":70,"text":556},{"id":729,"depth":70,"text":730},{"id":800,"depth":70,"text":801},{"id":1074,"depth":70,"text":1075},"how-to","2026-06-02","Firebase's web apiKey is not a secret. Your service account JSON key is. Step-by-step fix: identify what leaked, rotate the right credential, and lock your Security Rules.",false,"md",[1318,1320,1322,1324,1325],{"question":1221,"answer":1319},"The apiKey in your Firebase web config (starting with AIzaSy) is not a secret. Google intentionally makes it public. It just identifies your project so the SDK can connect. What's secret is your Firebase Admin SDK service account JSON key, which bypasses all Security Rules. Never commit the JSON file or use it in client-side code.",{"question":1231,"answer":1321},"Search your git history: git log --all --full-history -- '*.json' | head -20, then look for any file containing both 'private_key' and 'service_account'. Check your deployed JavaScript bundle in browser DevTools for 'FIREBASE_ADMIN' or 'private_key_id' strings. If your Admin SDK import appears in client-side code, the credentials are reachable.",{"question":1251,"answer":1323},"Any person on the internet with your project ID (which is public) can read and overwrite your entire Firestore or Realtime Database without logging in. In 2022, Cybernews researchers found 900+ Firebase apps in this state. Replace open rules with at minimum: allow read, write: if request.auth != null.",{"question":1260,"answer":1263},{"question":1266,"answer":1326},"In Google Cloud Console, go to IAM & Admin > Service Accounts, click the Firebase Admin SDK account, open the Keys tab, and add a new JSON key first. Update your server environment variable with the new credentials and redeploy. Once the new key is confirmed working, delete the old key. This zero-downtime rotation means production keeps running throughout.","yellow",null,"firebase api key exposure, fix firebase api key, firebase service account key leaked, firebase security rules open, firebase admin sdk server only, firebase credential rotation",{},"Firebase API key exposure fix: learn the public apiKey vs. service account distinction, rotate what actually needs rotating, and close open Security Rules.","/blog/how-to/fix-firebase-api-key-exposure","9 min read","[object Object]","HowTo",{"title":5,"description":1314},{"loc":1332},"blog/how-to/fix-firebase-api-key-exposure",[],"summary_large_image","2G2f2NySn4gBX0bjZxtTEiZPDzviqYV_6l2HvyM-xfg",1781823458173]