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).

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

  1. Stop brugen af nøglen så hurtigt som muligt. Disable eller revoke den i providerens dashboard.
  2. Lav en ny nøgle og opdater alle miljøer: lokal, test, preview, prod.
  3. Deploy igen, så appen faktisk bruger den nye nøgle.
  4. Gå historikken igennem: var nøglen i et offentligt repo? Hvor længe?
  5. 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

  1. Åbn .gitignore. Står .env.local og andre sensitive filer der?
  2. Kør git status. Er der nogen .env*-filer, der er tracked? Hvis ja, stop og fix.
  3. Tjek .env.example. Matcher den de variabler din app rent faktisk bruger?
  4. Søg i projektet efter ting som sk_live_, Bearer , eller kendte nøgle-prefixes.

Før du deployer

  1. Har du sat alle nødvendige env-vars i deploy-platformens UI?
  2. Bruger preview-miljø testnøgler og ikke produktions-nøgler?
  3. Har du mindst én secret der er forskellig i dev og prod (f.eks. JWT-secret)?
  4. 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

  1. Flyt hele kaldet til en backend-route du selv styrer, f.eks. et API-route i Next.js eller en lille Node/Express backend.
  2. Lad frontend kalde din egen backend uden at kende den rigtige API-nøgle.
  3. Rotér den lækkede nøgle hos provideren.
  4. Tilføj en regel: “Ingen eksterne API-secret keys i frontend” til dit README.

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.

Brug scanning-værktøjer som truffleHog, git-secrets eller GitHubs secret scanning til at lokalisere leaks i historikken. Hvis du finder noget, så revoce/rotate de kompromitterede nøgler straks, og ryd derefter historikken med BFG Repo-Cleaner eller git filter-repo og force-push til fjernlageret. Informér teamet, så ingen fortsætter med at bruge de gamle nøgler.
Sæt miljøvariabler i platformens eget secret/Environment UI eller via deres CLI, aldrig i kode eller commit. Marker tydeligt hvilke variabler der er for production/preview/build, undlad at logge værdier i build-logs, og sørg for at server-only secrets ikke eksponeres til klienten. Brug platformens krypterede storage og begræns adgang via roller hvor muligt.
Giv aldrig secrets til klienten - hold dem på serveren og brug server-endpoints eller server-side rendering til at hente følsomme data. Følg framework-konventioner (fx undlad NEXT_PUBLIC-prefix for hemmeligheder i Next.js) og undgå at interpolere secrets ind i filer, der bygges til klienten. Test build-artifakter for utilsigtede eksponeringer før deployment.
Generér secrets med kryptografisk sikre værktøjer (fx openssl rand -hex 32 eller sprog-specifikke crypto-API'er) og brug en længde på mindst ~32 bytes for tokens/keys. Opbevar dem i en secret manager (Vault, AWS Secrets Manager, GCP Secret Manager) eller en betroet password manager for teams, og implementér rotation og mindst-privilegium-adgang. Undgå at styre produktionssecrets i plaintext på serversystemer.

Lasse Falkenberg er typen, der begyndte at rode med HTML og CSS for at lave en simpel bandside – og opdagede, at det var langt sjovere at få knapperne til at virke end at stå på scenen. Siden har han kastet sig over alt fra små JavaScript-snippets til Python-scripts, der kan spare ham for kedeligt, manuelt arbejde i hverdagen.

Han har lært det meste ved at bygge ting, der lige præcis løser hans egne problemer: en lille webapp til at holde styr på brætspilsaftener, et script til at rydde op i rodede mapper, eller en enkel side til at dele noter med venner. Undervejs har han kæmpet sig gennem alle de klassiske fejl – semikolon, forkerte indrykninger og variabler, der hedder noget helt andet end man tror – og det er præcis den rejse, han deler på Coding Class.

På Coding Class skriver Lasse praktiske, jordnære guides, der tager udgangspunkt i små, konkrete opgaver: noget du kan se, teste og bygge videre på med det samme. Han elsker at bryde en opgave ned i små bidder, vise den fulde kode og forklare linje for linje, hvad der sker – inklusive de typiske bugs, du med stor sandsynlighed også støder på.

For Lasse handler kodning ikke om flotte titler eller store ord, men om følelsen af at få noget til at virke – og om at du som læser kan gå derfra med noget, du selv har bygget. Hvis du kan kende glæden ved at få en fejl til endelig at forsvinde, er du lige på bølgelængde med hans måde at lære fra sig på.

Send kommentar

You May Have Missed