Secrets skal være kedelige, ikke spændende
Du har allerede lækket noget, du bare ikke har opdaget det endnu
De fleste lækker deres første API-nøgle længe før de får deres første job som udvikler. Det starter uskyldigt: du vil bare hurtigt teste et API, smider nøglen direkte i koden, alt virker, du committer, pusher til GitHub og går tilbage til din kaffe.
Problemet er, at internettet ikke sover. Og bots, der scanner GitHub for nøgler, sover slet ikke.
Så lad os få styr på en simpel, sikker standard for miljøvariabler (environment variables), .env-filer og secrets, som du kan bruge fra nu af i alle dine projekter. Ikke som enterprise-security, bare som “jeg gider ikke regenere nøgler hver uge”-niveau.
1. Config vs secrets – to bunker, ikke én stor rodekasse
Første skridt er at dele din konfiguration op i to bunker: ting der må være kendt, og ting der skal behandles som adgangskoder.
Config: ufarlig, men miljøspecifik
Config er værdier, der kan være forskellige mellem udvikling og produktion, men som ikke i sig selv giver adgang til noget følsomt.
Eksempler:
- Base-URL til et API:
https://api.example.com - Feature flags:
FEATURE_SIGNUP_WIZARD=true - Logger-level:
LOG_LEVEL=debug - Port-numre:
PORT=3000
Hvis nogen får fat i de værdier, kan de måske gætte lidt om din opsætning, men de kan ikke direkte logge ind nogen steder.
Secrets: alt det, du ville være flov over at poste i et screenshot
Secrets er alt, der giver adgang til noget: systemer, data, brugere, penge.
Typiske secrets:
- API keys og tokens (Stripe, SendGrid, Supabase, osv.)
- Database-brugernavn og -password
- JWT-signing keys
- SSH-nøgler
- OAuth client secrets
Hvis du er i tvivl, så behandl det som en secret. Det er lidt ligesom køleskabet i et bofællesskab: hvis du er i tvivl om noget er fælles, så er det det sikkert ikke.
2. En simpel filstruktur der holder styr på dine miljøer
Miljøvariabler lyder fancy, men i hverdagen er det ofte bare en håndfuld .env-filer. Tricket er at bruge dem på en konsistent måde.
Standard-opstilling: tre filer, tre roller
Jeg plejer at bruge denne struktur:
.env.example
.env.local
.env.test
- .env.example: skabelon uden secrets. Viser hvilke variabler appen forventer.
- .env.local: dine lokale værdier, inkl. secrets. Kommer aldrig i git.
- .env.test: værdier til testmiljø, ofte med fake nøgler.
.env.example – din kontrakt til verden
.env.example skal være commit’et i repoet. Den beskriver, hvad appen kræver for at kunne starte.
# .env.example
# Public config
NEXT_PUBLIC_API_BASE_URL=https://api.example.com
# Backend only
DATABASE_URL=postgres://user:pass@host:5432/dbname
JWT_SECRET=<generate-long-random-string>
SENDGRID_API_KEY=<insert-key-here>
Der må ikke stå rigtige nøgler her. Brug tydelige placeholders som <insert-key-here>, så man ikke er i tvivl.
.env.local – dine hemmeligheder, kun på din maskine
.env.local er den fil du arbejder i til hverdag. Den skal ignoreres i .gitignore:
# .gitignore
.env.local
.env.test.local
Og indholdet kunne ligne:
# .env.local
NEXT_PUBLIC_API_BASE_URL=http://localhost:3000/api
DATABASE_URL=postgres://dev_user:dev_pass@localhost:5432/dev_db
JWT_SECRET=jf83hf9f0932hf90hf9032hf90hf9032hf
SENDGRID_API_KEY=SG.xxxxxx.yyyyyy
Hvis du nogensinde ser .env.local stå som “untracked” i git, så er det et lille rødt flag. Tjek to gange før du laver git add ..
3. Public vs private env – hvad må komme i frontend?
Frontend-kode bliver sendt ud til brugernes browser. Alt hvad der ligger i bundlet, er i praksis offentligt. Så public env-variabler må aldrig være rigtige secrets.
Public env: ting brugeren alligevel kan gætte
Public miljøvariabler er sådan noget frontend’en skal bruge for at tale med din backend eller vise noget config.
Eksempler på ok ting til public env:
- Base-URL til din egen backend:
NEXT_PUBLIC_API_BASE_URL - Feature flags, som bare ændrer UI
- Tracking-ID’er (ikke hemmelige nøgler) som Google Analytics ID
I mange frameworks kræver public env et prefix. I Vite er det VITE_, i Next.js er det NEXT_PUBLIC_. Det er med vilje. Brug det.
Private env: backend-only
Alt med adgang til eksterne services eller data skal ligge på backend-siden:
- Din Stripe secret key
- Din database connection string
- Auth-tokens til tredjeparts-API’er
Hvis du er i tvivl, så test sådan her: “Kan jeg forestille mig en HTTP-request, hvor en vilkårlig bruger skal sende den her værdi?” Hvis svaret er nej, er det backend-only.
4. Hvor miljøvariabler lever på Vercel, Netlify og venner
Lokalt har du .env.local. I produktion har du deploy-platformens UI eller CLI til at sætte miljøvariabler. De to verdener skal hænge sammen.
Vercel
På Vercel kan du sætte env-vars per miljø: Development, Preview, Production.
Typisk pattern:
- Development: matcher din lokale
.env.local - Preview: bruger test-/sandbox-nøgler
- Production: kun rigtige produktions-nøgler
Vigtig detalje: Vercel injicerer env-vars ved build-tid for frontend. Hvis du ændrer en variabel, skal du lave et nyt build for at få den med i bundlet.
Netlify
Samme idé, anden UI. Du sætter env-vars under “Site settings” → “Build & deploy” → “Environment”.
Her gælder også: frontend-variabler bliver bagt ind ved build. Backend-functions (Netlify Functions) læser dem ved runtime.
Hvordan previews kan gå galt
Problemet opstår ofte i preview-builds fra feature branches. Klassikeren:
- Du laver en branch, der tester noget med betaling.
- Previews bruger stadig production Stripe-nøgle.
- Du tester “bare” og kommer til at lave rigtige charges.
Derfor:
- Brug test/sandbox-nøgler i alle ikke-prod miljøer.
- Dokumenter i README hvilke nøgler der skal hvor.
- Overvej at slå visse flows fra i preview (f.eks. rigtig betaling).
På Coding Class har vi fx haft projekter hvor deploy-preview aldrig må tale med “rigtig” databasen, kun en kopi.
5. Rotation – når du (selvfølgelig) får lækket en nøgle
På et tidspunkt opdager du, at en nøgle er røget i et repo, en log eller et screenshot. Det er ikke sjovt, men det er heller ikke verdens undergang, hvis du har en proces.
En lille incident-proces i 5 skridt
- Stop brugen af nøglen så hurtigt som muligt. Disable eller revoke den i providerens dashboard.
- Lav en ny nøgle og opdater alle miljøer: lokal, test, preview, prod.
- Deploy igen, så appen faktisk bruger den nye nøgle.
- Gå historikken igennem: var nøglen i et offentligt repo? Hvor længe?
- Fix årsagen: manglede
.gitignore? Forkert logging? For bred adgang på nøglen?
Hvis du bruger services som Stripe, Supabase eller lignende, så tjek deres docs for “key rotation”. De har ofte specifikke anbefalinger.
6. Logging og fejlfinding uden at hælde secrets ud i logs
Logs er som en dagbog over hvad din app har lavet. En del udviklere har desværre for vane at hælde hele miljøet ud, når noget driller.
Log aldrig hele miljøet
Eksempler på ting du ikke skal gøre:
// Dårligt
console.log(process.env);
// Også dårligt
console.error("Config:", JSON.stringify(config));
Hvis du alligevel kommer til det lokalt, så sørg i det mindste for, at den log ikke lander i et delt system.
Log “formen” ikke indholdet
Bedre mønster:
// Godt: log kun at det er sat
console.log("Stripe key configured:", !!process.env.STRIPE_SECRET_KEY);
// Godt: maskér det meste
const key = process.env.STRIPE_SECRET_KEY;
console.log("Stripe key prefix:", key?.slice(0, 5));
På den måde kan du se, at der er en nøgle, uden at smide den i loggen.
Valider env-vars ved startup
En af de bedste vaner er at validere dine miljøvariabler, når appen starter. I stedet for at crashe halvvejs i en request.
// config.js
const required = [
"DATABASE_URL",
"JWT_SECRET",
"SENDGRID_API_KEY",
];
for (const name of required) {
if (!process.env[name]) {
throw new Error(`Missing required env var: ${name}`);
}
}
export const config = {
databaseUrl: process.env.DATABASE_URL,
jwtSecret: process.env.JWT_SECRET,
};
Her har du også et naturligt sted at dokumentere hvad der skal sættes. Matcher du det med din .env.example, har du en skarp kontrakt.
Hvis du vil dykke mere ned i mønstrene bag, er 12 Factor App om config og OWASP’s cheat sheets ret gode at have i baghånden.
7. Tjekliste før du pusher og før du deployer
Her er en lille to-trins tjekliste, du kan køre i hovedet (eller i dit README) hver gang.
Før du pusher
- Åbn
.gitignore. Står.env.localog andre sensitive filer der? - Kør
git status. Er der nogen.env*-filer, der er tracked? Hvis ja, stop og fix. - Tjek
.env.example. Matcher den de variabler din app rent faktisk bruger? - Søg i projektet efter ting som
sk_live_,Bearer, eller kendte nøgle-prefixes.
Før du deployer
- Har du sat alle nødvendige env-vars i deploy-platformens UI?
- Bruger preview-miljø testnøgler og ikke produktions-nøgler?
- Har du mindst én secret der er forskellig i dev og prod (f.eks. JWT-secret)?
- Er der nogen logs, der potentielt kan indeholde secrets? Har du maskeret dem?
Det her tager 1-2 minutter, når du har gjort det nogle gange. Tiden du sparer på ikke at rotere nøgler i panik, er noget større.
8. Mini-case – da min API-nøgle endte i bundlet
Lad os tage et lille scenarie, som jeg desværre har set lidt for ofte (inklusive en gang hvor det var min egen skyld).
Situationen
Du bygger en lille React-app, der skal kalde et tredjeparts-API direkte fra browseren. Du gør noget i stil med:
// api.js
const API_KEY = process.env.API_KEY;
export async function fetchData() {
const res = await fetch(`https://thirdparty.api/data?key=${API_KEY}`);
return res.json();
}
Du sætter API_KEY i din .env.local. Alt virker. Du deployer til Vercel, sætter den samme env-var der. Stadig fint.
Et par dage efter opdager du i devtools, at nøglen står direkte i netværkspanelet. Alle kan kopiere den.
Hvad skete der?
Build-systemet har taget værdien af process.env.API_KEY ved build-tid og skrevet den direkte ind i bundlet som en helt almindelig streng. For bundleren er det bare en konstant.
Du har altså gjort din secret til en del af det offentlige frontend-build.
Sådan opdager du det i tide
- Åbn devtools i prod og søg i koden efter kendte dele af dine nøgler.
- Byg projektet lokalt og søg i dist/build-mappen efter dele af dine secrets.
- Hold øje med, om dine API-kald går direkte til tredjeparts-tjenester fra browseren.
Sådan redder du den
- Flyt hele kaldet til en backend-route du selv styrer, f.eks. et API-route i Next.js eller en lille Node/Express backend.
- Lad frontend kalde din egen backend uden at kende den rigtige API-nøgle.
- Rotér den lækkede nøgle hos provideren.
- Tilføj en regel: “Ingen eksterne API-secret keys i frontend” til dit README.
På andre artikler om backend og sikkerhed prøver vi generelt at holde fast i den regel: frontend må kun snakke med ting, du er komfortabel med at brugeren ser.
9. Gør dine secrets så kedelige, at du glemmer de findes
Målet er egentlig, at du sjældent tænker over secrets. De skal ligge i deres egne bokse, være sat én gang per miljø og så bare være noget din app forventer er der.
Hvis du bruger en fast struktur med .env.example, .env.local, tydelig opdeling mellem public/private env og en lille rotation-proces, er du allerede foran en del professionelle teams, jeg har set.
Og hvis du en dag alligevel får lækket noget, så trøst dig med, at selv store virksomheder laver samme fejl. De skriver det bare i en lidt mere poleret post mortem, end man gør i sin README.









Send kommentar
Du skal være logget ind for at skrive en kommentar.