[{"data":1,"prerenderedAt":1488},["ShallowReactive",2],{"blog-how-to/deploy-netlify-securely":3},{"id":4,"title":5,"body":6,"category":1457,"date":1458,"dateModified":1458,"description":1459,"draft":1460,"extension":1461,"faq":1462,"featured":1460,"headerVariant":1473,"image":1474,"keywords":1475,"meta":1476,"navigation":169,"ogDescription":1477,"ogTitle":1474,"path":1478,"readTime":1479,"schemaOrg":1480,"schemaType":1481,"seo":1482,"sitemap":1483,"stem":1484,"tags":1485,"twitterCard":1486,"__hash__":1487},"blog/blog/how-to/deploy-netlify-securely.md","How to Deploy Netlify Securely (2026)",{"type":7,"value":8,"toc":1449},"minimark",[9,26,36,52,57,60,91,258,329,338,342,349,356,733,740,749,814,828,832,835,841,914,929,957,961,967,974,990,993,1000,1075,1078,1082,1085,1213,1229,1296,1300,1303,1306,1330,1333,1338,1418,1437,1445],[10,11,12,13,17,18,21,22,25],"p",{},"When Netlify builds your app, it clones your git repository and runs your build command. Any variable prefixed with ",[14,15,16],"code",{},"VITE_",", ",[14,19,20],{},"REACT_APP_",", or ",[14,23,24],{},"NEXT_PUBLIC_"," gets compiled into the JavaScript that ships to your visitors' browsers. That includes secrets you stored \"safely\" in Netlify's dashboard, because the prefix is an instruction to copy the value into the bundle regardless of where the variable came from.",[10,27,28,29,31,32,35],{},"In CheckYourVibe scans of Netlify-deployed apps, ",[14,30,16],{},"-prefixed secrets are the single most common exposure pattern, ahead of committed ",[14,33,34],{},".env"," files and hardcoded strings.",[37,38,39],"tldr",{},[10,40,41,42,44,45,47,48,51],{},"Netlify's biggest security risk is the ",[14,43,16],{}," (or ",[14,46,20],{},") prefix: it bakes secret keys into your client JavaScript at build time. Fix it by removing the prefix and moving the API call into a Netlify Function. Then add security headers via ",[14,49,50],{},"netlify.toml",", protect deploy previews with access control, scope production keys to the Production context only, and scan the live URL before going public.",[53,54,56],"h2",{"id":55},"step-1-audit-your-environment-variables","Step 1: Audit Your Environment Variables",[10,58,59],{},"Start by confirming what is actually in your JavaScript bundle.",[61,62,64],"step",{"number":63},"1",[10,65,66,70,71,74,75,78,79,82,83,86,87,90],{},[67,68,69],"strong",{},"Check the live bundle in DevTools."," Open your deployed Netlify app, press F12, go to Sources, and search (Cmd/Ctrl+F) for your key prefix: ",[14,72,73],{},"sk_proj_"," (OpenAI), ",[14,76,77],{},"sk_live_"," (Stripe), ",[14,80,81],{},"AKIA"," (AWS), ",[14,84,85],{},"eyJhbGci"," (Supabase JWTs). If it appears in any ",[14,88,89],{},".js"," file under your domain, it is in the client bundle.",[61,92,94,99,245],{"number":93},"2",[10,95,96],{},[67,97,98],{},"Search your codebase for public-prefixed secrets.",[100,101,103],"code-block",{"label":102},"bash",[104,105,109],"pre",{"className":106,"code":107,"language":102,"meta":108,"style":108},"language-bash shiki shiki-themes github-light github-dark","# Vite-based projects (React, Vue, Svelte)\ngrep -r \"VITE_\" . --include=\"*.env*\" --include=\"*.ts\" --include=\"*.tsx\" --include=\"*.js\" --include=\"*.vue\"\n\n# Create React App\ngrep -r \"REACT_APP_\" . --include=\"*.env*\" --include=\"*.ts\" --include=\"*.tsx\" --include=\"*.js\"\n\n# Next.js on Netlify\ngrep -r \"NEXT_PUBLIC_\" . --include=\"*.env*\" --include=\"*.ts\" --include=\"*.tsx\" --include=\"*.js\"\n","",[14,110,111,120,164,171,177,206,211,217],{"__ignoreMap":108},[112,113,116],"span",{"class":114,"line":115},"line",1,[112,117,119],{"class":118},"sJ8bj","# Vite-based projects (React, Vue, Svelte)\n",[112,121,123,127,131,135,138,141,144,146,149,151,154,156,159,161],{"class":114,"line":122},2,[112,124,126],{"class":125},"sScJk","grep",[112,128,130],{"class":129},"sj4cs"," -r",[112,132,134],{"class":133},"sZZnC"," \"VITE_\"",[112,136,137],{"class":133}," .",[112,139,140],{"class":129}," --include=",[112,142,143],{"class":133},"\"*.env*\"",[112,145,140],{"class":129},[112,147,148],{"class":133},"\"*.ts\"",[112,150,140],{"class":129},[112,152,153],{"class":133},"\"*.tsx\"",[112,155,140],{"class":129},[112,157,158],{"class":133},"\"*.js\"",[112,160,140],{"class":129},[112,162,163],{"class":133},"\"*.vue\"\n",[112,165,167],{"class":114,"line":166},3,[112,168,170],{"emptyLinePlaceholder":169},true,"\n",[112,172,174],{"class":114,"line":173},4,[112,175,176],{"class":118},"# Create React App\n",[112,178,180,182,184,187,189,191,193,195,197,199,201,203],{"class":114,"line":179},5,[112,181,126],{"class":125},[112,183,130],{"class":129},[112,185,186],{"class":133}," \"REACT_APP_\"",[112,188,137],{"class":133},[112,190,140],{"class":129},[112,192,143],{"class":133},[112,194,140],{"class":129},[112,196,148],{"class":133},[112,198,140],{"class":129},[112,200,153],{"class":133},[112,202,140],{"class":129},[112,204,205],{"class":133},"\"*.js\"\n",[112,207,209],{"class":114,"line":208},6,[112,210,170],{"emptyLinePlaceholder":169},[112,212,214],{"class":114,"line":213},7,[112,215,216],{"class":118},"# Next.js on Netlify\n",[112,218,220,222,224,227,229,231,233,235,237,239,241,243],{"class":114,"line":219},8,[112,221,126],{"class":125},[112,223,130],{"class":129},[112,225,226],{"class":133}," \"NEXT_PUBLIC_\"",[112,228,137],{"class":133},[112,230,140],{"class":129},[112,232,143],{"class":133},[112,234,140],{"class":129},[112,236,148],{"class":133},[112,238,140],{"class":129},[112,240,153],{"class":133},[112,242,140],{"class":129},[112,244,205],{"class":133},[10,246,247,248,17,251,21,254,257],{},"Any variable named ",[14,249,250],{},"VITE_OPENAI_API_KEY",[14,252,253],{},"REACT_APP_STRIPE_SECRET",[14,255,256],{},"NEXT_PUBLIC_ANTHROPIC_KEY"," is in your visitor's browser. The Netlify dashboard doesn't change this: the prefix instructs the build tool to embed the value regardless of where it came from.",[61,259,261,269,326],{"number":260},"3",[10,262,263],{},[67,264,265,266,268],{},"Check your git history for committed ",[14,267,34],{}," files.",[100,270,271],{"label":102},[104,272,274],{"className":106,"code":273,"language":102,"meta":108,"style":108},"git log --all --full-history -- .env\ngit log --all --full-history -- .env.local\ngit log --all --full-history -- .env.production\n",[14,275,276,296,311],{"__ignoreMap":108},[112,277,278,281,284,287,290,293],{"class":114,"line":115},[112,279,280],{"class":125},"git",[112,282,283],{"class":133}," log",[112,285,286],{"class":129}," --all",[112,288,289],{"class":129}," --full-history",[112,291,292],{"class":129}," --",[112,294,295],{"class":133}," .env\n",[112,297,298,300,302,304,306,308],{"class":114,"line":122},[112,299,280],{"class":125},[112,301,283],{"class":133},[112,303,286],{"class":129},[112,305,289],{"class":129},[112,307,292],{"class":129},[112,309,310],{"class":133}," .env.local\n",[112,312,313,315,317,319,321,323],{"class":114,"line":166},[112,314,280],{"class":125},[112,316,283],{"class":133},[112,318,286],{"class":129},[112,320,289],{"class":129},[112,322,292],{"class":129},[112,324,325],{"class":133}," .env.production\n",[10,327,328],{},"If these return any commits, those secrets were readable during every Netlify CI build that included them. Anyone who cloned the repo has a copy.",[330,331,332],"warning-box",{},[10,333,334,335,337],{},"The Netlify dashboard shows you which variables are set, not which ones are safe. A variable stored in the dashboard with a ",[14,336,16],{}," prefix is just as exposed as one hardcoded in source code.",[53,339,341],{"id":340},"step-2-move-secret-api-calls-to-netlify-functions","Step 2: Move Secret API Calls to Netlify Functions",[10,343,344,345,348],{},"Netlify Functions run in a Node.js environment on your domain. They read environment variables at runtime via ",[14,346,347],{},"process.env",", never at build time, so nothing ends up in the client bundle.",[10,350,351,352,355],{},"Create a ",[14,353,354],{},"netlify/functions/"," directory in your repo root. Netlify deploys everything in it automatically.",[100,357,359],{"label":358},"netlify/functions/chat.ts",[104,360,364],{"className":361,"code":362,"language":363,"meta":108,"style":108},"language-typescript shiki shiki-themes github-light github-dark","import { Handler } from \"@netlify/functions\";\nimport OpenAI from \"openai\";\n\n// process.env.OPENAI_API_KEY (no VITE_ prefix)\nconst client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });\n\nexport const handler: Handler = async (event) => {\n  if (event.httpMethod !== \"POST\") {\n    return { statusCode: 405, body: \"Method Not Allowed\" };\n  }\n\n  const { message } = JSON.parse(event.body || \"{}\");\n  if (!message) {\n    return { statusCode: 400, body: \"message required\" };\n  }\n\n  const response = await client.chat.completions.create({\n    model: \"gpt-4o\",\n    messages: [{ role: \"user\", content: message }],\n  });\n\n  return {\n    statusCode: 200,\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({ reply: response.choices[0].message.content }),\n  };\n};\n","typescript",[14,365,366,385,399,403,408,434,438,476,493,514,520,525,564,577,594,599,604,626,638,650,656,661,669,680,698,721,727],{"__ignoreMap":108},[112,367,368,372,376,379,382],{"class":114,"line":115},[112,369,371],{"class":370},"szBVR","import",[112,373,375],{"class":374},"sVt8B"," { Handler } ",[112,377,378],{"class":370},"from",[112,380,381],{"class":133}," \"@netlify/functions\"",[112,383,384],{"class":374},";\n",[112,386,387,389,392,394,397],{"class":114,"line":122},[112,388,371],{"class":370},[112,390,391],{"class":374}," OpenAI ",[112,393,378],{"class":370},[112,395,396],{"class":133}," \"openai\"",[112,398,384],{"class":374},[112,400,401],{"class":114,"line":166},[112,402,170],{"emptyLinePlaceholder":169},[112,404,405],{"class":114,"line":173},[112,406,407],{"class":118},"// process.env.OPENAI_API_KEY (no VITE_ prefix)\n",[112,409,410,413,416,419,422,425,428,431],{"class":114,"line":179},[112,411,412],{"class":370},"const",[112,414,415],{"class":129}," client",[112,417,418],{"class":370}," =",[112,420,421],{"class":370}," new",[112,423,424],{"class":125}," OpenAI",[112,426,427],{"class":374},"({ apiKey: process.env.",[112,429,430],{"class":129},"OPENAI_API_KEY",[112,432,433],{"class":374}," });\n",[112,435,436],{"class":114,"line":208},[112,437,170],{"emptyLinePlaceholder":169},[112,439,440,443,446,449,452,455,457,460,463,467,470,473],{"class":114,"line":213},[112,441,442],{"class":370},"export",[112,444,445],{"class":370}," const",[112,447,448],{"class":125}," handler",[112,450,451],{"class":370},":",[112,453,454],{"class":125}," Handler",[112,456,418],{"class":370},[112,458,459],{"class":370}," async",[112,461,462],{"class":374}," (",[112,464,466],{"class":465},"s4XuR","event",[112,468,469],{"class":374},") ",[112,471,472],{"class":370},"=>",[112,474,475],{"class":374}," {\n",[112,477,478,481,484,487,490],{"class":114,"line":219},[112,479,480],{"class":370},"  if",[112,482,483],{"class":374}," (event.httpMethod ",[112,485,486],{"class":370},"!==",[112,488,489],{"class":133}," \"POST\"",[112,491,492],{"class":374},") {\n",[112,494,496,499,502,505,508,511],{"class":114,"line":495},9,[112,497,498],{"class":370},"    return",[112,500,501],{"class":374}," { statusCode: ",[112,503,504],{"class":129},"405",[112,506,507],{"class":374},", body: ",[112,509,510],{"class":133},"\"Method Not Allowed\"",[112,512,513],{"class":374}," };\n",[112,515,517],{"class":114,"line":516},10,[112,518,519],{"class":374},"  }\n",[112,521,523],{"class":114,"line":522},11,[112,524,170],{"emptyLinePlaceholder":169},[112,526,528,531,534,537,540,543,546,549,552,555,558,561],{"class":114,"line":527},12,[112,529,530],{"class":370},"  const",[112,532,533],{"class":374}," { ",[112,535,536],{"class":129},"message",[112,538,539],{"class":374}," } ",[112,541,542],{"class":370},"=",[112,544,545],{"class":129}," JSON",[112,547,548],{"class":374},".",[112,550,551],{"class":125},"parse",[112,553,554],{"class":374},"(event.body ",[112,556,557],{"class":370},"||",[112,559,560],{"class":133}," \"{}\"",[112,562,563],{"class":374},");\n",[112,565,567,569,571,574],{"class":114,"line":566},13,[112,568,480],{"class":370},[112,570,462],{"class":374},[112,572,573],{"class":370},"!",[112,575,576],{"class":374},"message) {\n",[112,578,580,582,584,587,589,592],{"class":114,"line":579},14,[112,581,498],{"class":370},[112,583,501],{"class":374},[112,585,586],{"class":129},"400",[112,588,507],{"class":374},[112,590,591],{"class":133},"\"message required\"",[112,593,513],{"class":374},[112,595,597],{"class":114,"line":596},15,[112,598,519],{"class":374},[112,600,602],{"class":114,"line":601},16,[112,603,170],{"emptyLinePlaceholder":169},[112,605,607,609,612,614,617,620,623],{"class":114,"line":606},17,[112,608,530],{"class":370},[112,610,611],{"class":129}," response",[112,613,418],{"class":370},[112,615,616],{"class":370}," await",[112,618,619],{"class":374}," client.chat.completions.",[112,621,622],{"class":125},"create",[112,624,625],{"class":374},"({\n",[112,627,629,632,635],{"class":114,"line":628},18,[112,630,631],{"class":374},"    model: ",[112,633,634],{"class":133},"\"gpt-4o\"",[112,636,637],{"class":374},",\n",[112,639,641,644,647],{"class":114,"line":640},19,[112,642,643],{"class":374},"    messages: [{ role: ",[112,645,646],{"class":133},"\"user\"",[112,648,649],{"class":374},", content: message }],\n",[112,651,653],{"class":114,"line":652},20,[112,654,655],{"class":374},"  });\n",[112,657,659],{"class":114,"line":658},21,[112,660,170],{"emptyLinePlaceholder":169},[112,662,664,667],{"class":114,"line":663},22,[112,665,666],{"class":370},"  return",[112,668,475],{"class":374},[112,670,672,675,678],{"class":114,"line":671},23,[112,673,674],{"class":374},"    statusCode: ",[112,676,677],{"class":129},"200",[112,679,637],{"class":374},[112,681,683,686,689,692,695],{"class":114,"line":682},24,[112,684,685],{"class":374},"    headers: { ",[112,687,688],{"class":133},"\"Content-Type\"",[112,690,691],{"class":374},": ",[112,693,694],{"class":133},"\"application/json\"",[112,696,697],{"class":374}," },\n",[112,699,701,704,707,709,712,715,718],{"class":114,"line":700},25,[112,702,703],{"class":374},"    body: ",[112,705,706],{"class":129},"JSON",[112,708,548],{"class":374},[112,710,711],{"class":125},"stringify",[112,713,714],{"class":374},"({ reply: response.choices[",[112,716,717],{"class":129},"0",[112,719,720],{"class":374},"].message.content }),\n",[112,722,724],{"class":114,"line":723},26,[112,725,726],{"class":374},"  };\n",[112,728,730],{"class":114,"line":729},27,[112,731,732],{"class":374},"};\n",[10,734,735,736,739],{},"Your React component calls ",[14,737,738],{},"/.netlify/functions/chat"," on your own domain. The function calls OpenAI. The key never reaches the browser.",[10,741,742,743,745,746,748],{},"In the Netlify dashboard, rename ",[14,744,250],{}," to ",[14,747,430],{}," (no prefix). The variable now scopes to Functions only.",[750,751,752,765],"table",{},[753,754,755],"thead",{},[756,757,758,762],"tr",{},[759,760,761],"th",{},"Old (exposed)",[759,763,764],{},"New (safe)",[766,767,768,779,790,802],"tbody",{},[756,769,770,775],{},[771,772,773],"td",{},[14,774,250],{},[771,776,777],{},[14,778,430],{},[756,780,781,785],{},[771,782,783],{},[14,784,253],{},[771,786,787],{},[14,788,789],{},"STRIPE_SECRET_KEY",[756,791,792,797],{},[771,793,794],{},[14,795,796],{},"VITE_ANTHROPIC_API_KEY",[771,798,799],{},[14,800,801],{},"ANTHROPIC_API_KEY",[756,803,804,809],{},[771,805,806],{},[14,807,808],{},"VITE_SUPABASE_SERVICE_ROLE_KEY",[771,810,811],{},[14,812,813],{},"SUPABASE_SERVICE_ROLE_KEY",[10,815,816,817,820,821,823,824,827],{},"The Supabase ",[14,818,819],{},"anon"," key is designed to be public and safe with ",[14,822,16],{}," as long as you have Row Level Security enabled on every table. The ",[14,825,826],{},"service_role"," key bypasses RLS entirely and must never reach the browser.",[53,829,831],{"id":830},"step-3-set-security-headers-via-netlifytoml","Step 3: Set Security Headers via netlify.toml",[10,833,834],{},"Netlify does not add security headers by default. Without them, your site ships without clickjacking protection, content type sniffing prevention, or HSTS.",[10,836,837,838,840],{},"Add or update ",[14,839,50],{}," in your repository root:",[100,842,843],{"label":50},[104,844,848],{"className":845,"code":846,"language":847,"meta":108,"style":108},"language-toml shiki shiki-themes github-light github-dark","[build]\n  command = \"npm run build\"   # or bun run build, vite build, etc.\n  publish = \"dist\"            # or build/, public/, .next/\n\n[[headers]]\n  for = \"/*\"\n  [headers.values]\n    X-Frame-Options = \"DENY\"\n    X-Content-Type-Options = \"nosniff\"\n    Referrer-Policy = \"strict-origin-when-cross-origin\"\n    Permissions-Policy = \"camera=(), microphone=(), geolocation=()\"\n    Strict-Transport-Security = \"max-age=31536000; includeSubDomains\"\n    Content-Security-Policy = \"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https:;\"\n","toml",[14,849,850,855,860,865,869,874,879,884,889,894,899,904,909],{"__ignoreMap":108},[112,851,852],{"class":114,"line":115},[112,853,854],{},"[build]\n",[112,856,857],{"class":114,"line":122},[112,858,859],{},"  command = \"npm run build\"   # or bun run build, vite build, etc.\n",[112,861,862],{"class":114,"line":166},[112,863,864],{},"  publish = \"dist\"            # or build/, public/, .next/\n",[112,866,867],{"class":114,"line":173},[112,868,170],{"emptyLinePlaceholder":169},[112,870,871],{"class":114,"line":179},[112,872,873],{},"[[headers]]\n",[112,875,876],{"class":114,"line":208},[112,877,878],{},"  for = \"/*\"\n",[112,880,881],{"class":114,"line":213},[112,882,883],{},"  [headers.values]\n",[112,885,886],{"class":114,"line":219},[112,887,888],{},"    X-Frame-Options = \"DENY\"\n",[112,890,891],{"class":114,"line":495},[112,892,893],{},"    X-Content-Type-Options = \"nosniff\"\n",[112,895,896],{"class":114,"line":516},[112,897,898],{},"    Referrer-Policy = \"strict-origin-when-cross-origin\"\n",[112,900,901],{"class":114,"line":522},[112,902,903],{},"    Permissions-Policy = \"camera=(), microphone=(), geolocation=()\"\n",[112,905,906],{"class":114,"line":527},[112,907,908],{},"    Strict-Transport-Security = \"max-age=31536000; includeSubDomains\"\n",[112,910,911],{"class":114,"line":566},[112,912,913],{},"    Content-Security-Policy = \"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https:;\"\n",[10,915,916,917,920,921,924,925,928],{},"The CSP above is a starting baseline. Tighten ",[14,918,919],{},"script-src"," by removing ",[14,922,923],{},"'unsafe-inline'"," if your app doesn't use inline scripts, and restrict ",[14,926,927],{},"connect-src"," to specific domains your app actually calls.",[930,931,932],"tip-box",{},[10,933,934,935,937,938,941,942,17,945,948,949,952,953,956],{},"After deploying with the updated ",[14,936,50],{},", verify the headers are live: ",[14,939,940],{},"curl -I https://yourdomain.netlify.app"," and check that ",[14,943,944],{},"x-frame-options",[14,946,947],{},"x-content-type-options",", and ",[14,950,951],{},"strict-transport-security"," appear in the response. Netlify strips custom headers for local ",[14,954,955],{},"netlify dev"," sessions, so always test against the live deployment.",[53,958,960],{"id":959},"step-4-protect-deploy-previews","Step 4: Protect Deploy Previews",[10,962,963,964,966],{},"Every pull request generates a public Netlify preview URL. Any ",[14,965,16],{},"-prefixed variable in that build is visible at that URL. By default, these URLs have no access control.",[10,968,969,970,973],{},"Go to ",[67,971,972],{},"Site Configuration > Access Control"," and enable one of:",[975,976,977,984],"ul",{},[978,979,980,983],"li",{},[67,981,982],{},"Site password",": visitors enter a password before seeing the preview",[978,985,986,989],{},[67,987,988],{},"Netlify Identity",": visitors must sign in with a Netlify Identity account",[10,991,992],{},"For most teams, a site password on deploy previews is the simplest option. It prevents the preview URL from being a live demo of your app using production credentials.",[10,994,995,996,999],{},"Once you've protected previews, also set preview-specific environment variables. In ",[67,997,998],{},"Site Configuration > Environment Variables",", scope your secret keys:",[750,1001,1002,1018],{},[753,1003,1004],{},[756,1005,1006,1009,1012,1015],{},[759,1007,1008],{},"Variable",[759,1010,1011],{},"Production",[759,1013,1014],{},"Deploy Previews",[759,1016,1017],{},"Branch deploys",[766,1019,1020,1040,1060],{},[756,1021,1022,1026,1031,1036],{},[771,1023,1024],{},[14,1025,430],{},[771,1027,1028],{},[14,1029,1030],{},"sk-prod-real-key",[771,1032,1033],{},[14,1034,1035],{},"sk-test-safe-key",[771,1037,1038],{},[14,1039,1035],{},[756,1041,1042,1046,1051,1056],{},[771,1043,1044],{},[14,1045,789],{},[771,1047,1048],{},[14,1049,1050],{},"sk_live_...",[771,1052,1053],{},[14,1054,1055],{},"sk_test_...",[771,1057,1058],{},[14,1059,1055],{},[756,1061,1062,1067,1070,1073],{},[771,1063,1064],{},[14,1065,1066],{},"DATABASE_URL",[771,1068,1069],{},"prod DB",[771,1071,1072],{},"staging DB",[771,1074,1072],{},[10,1076,1077],{},"A leaked preview URL then exposes test credentials, not production ones.",[53,1079,1081],{"id":1080},"step-5-add-a-pre-commit-hook","Step 5: Add a Pre-commit Hook",[10,1083,1084],{},"This prevents the VITE_ leak from recurring.",[100,1086,1087],{"label":102},[104,1088,1090],{"className":106,"code":1089,"language":102,"meta":108,"style":108},"# Install gitleaks\nbrew install gitleaks   # macOS\n# or: pip install gitleaks, or download from github.com/gitleaks/gitleaks/releases\n\n# Test current repo\ngitleaks detect --source . --verbose\n\n# Add pre-commit hook\ncat > .git/hooks/pre-commit \u003C\u003C 'HOOKEOF'\n#!/bin/sh\ngitleaks protect --staged --verbose\nif [ $? -ne 0 ]; then\n  echo \"gitleaks found secrets in staged files. Commit blocked.\"\n  exit 1\nfi\nHOOKEOF\nchmod +x .git/hooks/pre-commit\n",[14,1091,1092,1097,1111,1116,1120,1125,1141,1145,1150,1167,1172,1177,1182,1187,1192,1197,1202],{"__ignoreMap":108},[112,1093,1094],{"class":114,"line":115},[112,1095,1096],{"class":118},"# Install gitleaks\n",[112,1098,1099,1102,1105,1108],{"class":114,"line":122},[112,1100,1101],{"class":125},"brew",[112,1103,1104],{"class":133}," install",[112,1106,1107],{"class":133}," gitleaks",[112,1109,1110],{"class":118},"   # macOS\n",[112,1112,1113],{"class":114,"line":166},[112,1114,1115],{"class":118},"# or: pip install gitleaks, or download from github.com/gitleaks/gitleaks/releases\n",[112,1117,1118],{"class":114,"line":173},[112,1119,170],{"emptyLinePlaceholder":169},[112,1121,1122],{"class":114,"line":179},[112,1123,1124],{"class":118},"# Test current repo\n",[112,1126,1127,1130,1133,1136,1138],{"class":114,"line":208},[112,1128,1129],{"class":125},"gitleaks",[112,1131,1132],{"class":133}," detect",[112,1134,1135],{"class":129}," --source",[112,1137,137],{"class":133},[112,1139,1140],{"class":129}," --verbose\n",[112,1142,1143],{"class":114,"line":213},[112,1144,170],{"emptyLinePlaceholder":169},[112,1146,1147],{"class":114,"line":219},[112,1148,1149],{"class":118},"# Add pre-commit hook\n",[112,1151,1152,1155,1158,1161,1164],{"class":114,"line":495},[112,1153,1154],{"class":125},"cat",[112,1156,1157],{"class":370}," >",[112,1159,1160],{"class":133}," .git/hooks/pre-commit",[112,1162,1163],{"class":370}," \u003C\u003C",[112,1165,1166],{"class":133}," 'HOOKEOF'\n",[112,1168,1169],{"class":114,"line":516},[112,1170,1171],{"class":133},"#!/bin/sh\n",[112,1173,1174],{"class":114,"line":522},[112,1175,1176],{"class":133},"gitleaks protect --staged --verbose\n",[112,1178,1179],{"class":114,"line":527},[112,1180,1181],{"class":133},"if [ $? -ne 0 ]; then\n",[112,1183,1184],{"class":114,"line":566},[112,1185,1186],{"class":133},"  echo \"gitleaks found secrets in staged files. Commit blocked.\"\n",[112,1188,1189],{"class":114,"line":579},[112,1190,1191],{"class":133},"  exit 1\n",[112,1193,1194],{"class":114,"line":596},[112,1195,1196],{"class":133},"fi\n",[112,1198,1199],{"class":114,"line":601},[112,1200,1201],{"class":133},"HOOKEOF\n",[112,1203,1204,1207,1210],{"class":114,"line":606},[112,1205,1206],{"class":125},"chmod",[112,1208,1209],{"class":133}," +x",[112,1211,1212],{"class":133}," .git/hooks/pre-commit\n",[10,1214,1215,1216,17,1218,948,1221,1224,1225,1228],{},"Add ",[14,1217,34],{},[14,1219,1220],{},".env.local",[14,1222,1223],{},".env.production"," to your ",[14,1226,1227],{},".gitignore"," if they aren't already:",[100,1230,1231],{"label":102},[104,1232,1234],{"className":106,"code":1233,"language":102,"meta":108,"style":108},"echo \".env\" >> .gitignore\necho \".env*.local\" >> .gitignore\necho \".env.production\" >> .gitignore\ngit add .gitignore && git commit -m \"chore: gitignore .env files\"\n",[14,1235,1236,1250,1261,1272],{"__ignoreMap":108},[112,1237,1238,1241,1244,1247],{"class":114,"line":115},[112,1239,1240],{"class":129},"echo",[112,1242,1243],{"class":133}," \".env\"",[112,1245,1246],{"class":370}," >>",[112,1248,1249],{"class":133}," .gitignore\n",[112,1251,1252,1254,1257,1259],{"class":114,"line":122},[112,1253,1240],{"class":129},[112,1255,1256],{"class":133}," \".env*.local\"",[112,1258,1246],{"class":370},[112,1260,1249],{"class":133},[112,1262,1263,1265,1268,1270],{"class":114,"line":166},[112,1264,1240],{"class":129},[112,1266,1267],{"class":133}," \".env.production\"",[112,1269,1246],{"class":370},[112,1271,1249],{"class":133},[112,1273,1274,1276,1279,1282,1285,1287,1290,1293],{"class":114,"line":173},[112,1275,280],{"class":125},[112,1277,1278],{"class":133}," add",[112,1280,1281],{"class":133}," .gitignore",[112,1283,1284],{"class":374}," && ",[112,1286,280],{"class":125},[112,1288,1289],{"class":133}," commit",[112,1291,1292],{"class":129}," -m",[112,1294,1295],{"class":133}," \"chore: gitignore .env files\"\n",[53,1297,1299],{"id":1298},"step-6-scan-before-going-live","Step 6: Scan Before Going Live",[10,1301,1302],{},"Before you remove the under-construction page or announce your launch, run a CheckYourVibe scan on the live Netlify URL.",[10,1304,1305],{},"The scanner checks:",[975,1307,1308,1311,1314,1320,1327],{},[978,1309,1310],{},"JavaScript bundle for embedded API keys (OpenAI, Stripe, Supabase, AWS, and 30+ others)",[978,1312,1313],{},"HTTP response headers against the OWASP recommended set",[978,1315,1316,1317,1319],{},"Supabase ",[14,1318,826],{}," key used client-side",[978,1321,1322,1323,1326],{},"CORS policy set to ",[14,1324,1325],{},"*"," on your Netlify Functions",[978,1328,1329],{},"Mixed content and insecure resource loading",[10,1331,1332],{},"It takes about 90 seconds and gives you a prioritized list. Fix the criticals before launch, not after your first real users arrive.",[930,1334,1335],{},[10,1336,1337],{},"If your Netlify app is still behind a maintenance page or access control, you can scan the deploy preview URL directly. CheckYourVibe follows authentication headers you provide.",[1339,1340,1341,1356,1376,1388,1412],"faq-section",{},[1342,1343,1345],"faq-item",{"question":1344},"Are Netlify environment variables safe?",[10,1346,1347,1348,1351,1352,1355],{},"Variables stored in Netlify's dashboard without a public prefix are encrypted and only available in Netlify Functions at runtime. They're never compiled into the client bundle. The prefix is what makes them unsafe: ",[14,1349,1350],{},"VITE_MY_SECRET=value"," tells Vite to embed ",[14,1353,1354],{},"value"," into the output JavaScript regardless of where the variable came from.",[1342,1357,1359],{"question":1358},"What's the difference between VITE_ and process.env in Netlify?",[10,1360,1361,1363,1364,1367,1368,1370,1371,1373,1374,548],{},[14,1362,16],{}," variables are substituted at build time: Vite replaces every reference to ",[14,1365,1366],{},"import.meta.env.VITE_FOO"," with the raw string value in the compiled JavaScript. ",[14,1369,347],{}," variables inside a Netlify Function are resolved at runtime in a Node.js environment on Netlify's servers. The value never touches the browser. Secret keys must use ",[14,1372,347],{}," inside a Function, never ",[14,1375,16],{},[1342,1377,1379],{"question":1378},"Do Netlify deploy previews expose my environment variables?",[10,1380,1381,1382,1384,1385,1387],{},"Yes, if those variables have a ",[14,1383,16],{}," or ",[14,1386,20],{}," prefix. Every pull request triggers a public preview build that includes your environment variables. Without preview access control, anyone with the URL can open DevTools and read those values. Scope production keys to the Production context and use test credentials for previews.",[1342,1389,1391],{"question":1390},"How do I add security headers to a Netlify site?",[10,1392,1393,1394,1397,1398,1400,1401,1404,1405,1408,1409,1411],{},"Add a ",[14,1395,1396],{},"[[headers]]"," block to ",[14,1399,50],{}," targeting ",[14,1402,1403],{},"/*",". Headers defined there apply to every response from your site. You can also create a ",[14,1406,1407],{},"_headers"," file in your publish directory using the same header name/value syntax. ",[14,1410,50],{}," takes precedence when both files exist.",[1342,1413,1415],{"question":1414},"What should I scan for before launching on Netlify?",[10,1416,1417],{},"Check your live JavaScript bundle for exposed API keys (any VITE_-prefixed secret), confirm security headers appear in actual HTTP responses, verify deploy previews require authentication, and test that your Netlify Functions return 401 for unauthenticated requests on any endpoint that reads or writes your database.",[1419,1420,1421,1427,1432],"related-articles",{},[1422,1423],"related-card",{"description":1424,"href":1425,"title":1426},"Already shipped with a leaked key? Find VITE_ or REACT_APP_ secrets in your bundle, rotate them, and move the call to Netlify Functions.","/blog/how-to/fix-netlify-api-key-exposure","How to Fix Netlify API Key Exposure (2026)",[1422,1428],{"description":1429,"href":1430,"title":1431},"Deep dive into the _headers file and netlify.toml approach for adding Content-Security-Policy, HSTS, and every OWASP recommended header.","/blog/how-to/netlify-headers","How to Configure Security Headers on Netlify",[1422,1433],{"description":1434,"href":1435,"title":1436},"Print-friendly checklist covering environment variables, security headers, Netlify Functions, deploy previews, and team access controls.","/blog/checklists/netlify-security-checklist","Netlify Security Checklist: 15-Item Guide",[1438,1439,1442],"cta-box",{"href":1440,"label":1441},"/","Scan Your Netlify App",[10,1443,1444],{},"CheckYourVibe scans your live Netlify deployment for exposed API keys in the JavaScript bundle, missing security headers, and Supabase service_role misuse. Free scan, no signup required.",[1446,1447,1448],"style",{},"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 .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 .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 .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":108,"searchDepth":122,"depth":122,"links":1450},[1451,1452,1453,1454,1455,1456],{"id":55,"depth":122,"text":56},{"id":340,"depth":122,"text":341},{"id":830,"depth":122,"text":831},{"id":959,"depth":122,"text":960},{"id":1080,"depth":122,"text":1081},{"id":1298,"depth":122,"text":1299},"how-to","2026-06-08","Deploy your Netlify app without leaking API keys, exposing deploy previews, or shipping misconfigured security headers. Six steps: audit VITE_ env vars, move secrets to Functions, set headers via netlify.toml, protect previews, scope variables by context, and scan before launch.",false,"md",[1463,1465,1467,1469,1471],{"question":1344,"answer":1464},"Variables stored in Netlify's dashboard without a public prefix (VITE_, REACT_APP_, NEXT_PUBLIC_) are encrypted and only available in Netlify Functions at runtime. They are not baked into the client bundle. If you add a VITE_ prefix to a variable, Netlify's build system compiles the value into your JavaScript, where any visitor can read it.",{"question":1358,"answer":1466},"VITE_ variables are replaced at build time: Vite reads the value and writes it directly into the output JavaScript. process.env variables are resolved at runtime inside a Netlify Function (a Node.js serverless environment), so the value never touches the browser. Secret keys must use process.env inside a Function, never VITE_.",{"question":1378,"answer":1468},"Yes, if those variables have a VITE_ or REACT_APP_ prefix. Every pull request triggers a public preview build using your environment variables. Without preview access control enabled, anyone with the URL can open DevTools and read those values. Use preview-specific test credentials scoped to the Deploy Previews context.",{"question":1390,"answer":1470},"Add a netlify.toml file to your repository root with a [[headers]] block targeting /*. Headers you set there apply to every response from your site. Alternatively, create a _headers file in your publish directory (usually dist/ or public/). netlify.toml takes precedence when both exist.",{"question":1414,"answer":1472},"Check your live JavaScript bundle for exposed API keys (especially VITE_-prefixed OpenAI, Stripe, or Supabase service_role values), confirm security headers are present in actual HTTP responses (not just netlify.toml), check that deploy previews require authentication, and verify Netlify Functions return 401 for unauthenticated requests on any endpoint that touches your database.","yellow",null,"deploy netlify securely, netlify security deployment, netlify environment variables security, netlify functions api keys, netlify deploy preview protection, netlify.toml headers, netlify secure deploy guide",{},"Netlify secure deployment guide: fix VITE_ env var leaks, move API calls to Netlify Functions, add security headers via netlify.toml, and protect deploy previews.","/blog/how-to/deploy-netlify-securely","12 min read","[object Object]","HowTo",{"title":5,"description":1459},{"loc":1478},"blog/how-to/deploy-netlify-securely",[],"summary_large_image","AEM4QNZane1GdiMcL-Mz3sBgaiS6j03Ybdqz6RgvIQo",1784736388812]