Spring til indhold
MetodeIndsigter

Preview-secrets og produktion — samme nøgle er et læk

I har skilt miljøerne, når preview ikke kan nå det, produktion skylder at holde hemmeligt.

En preview-URL er delt. En produktionsnøgle dér er ikke 'praktisk'. Det er hvordan I giver en pull request adgang til rigtige kunder.

13 minutters læsning
Stiliseret CI-pipeline med checks og gates — et preview-miljø med egne nøgler versus produktion, ikke én hemmelighed I kopierede til alle URL'er.

DevOps & CI

Hvis I søger efter preview secrets, eller efter Vercel environment variables, står I typisk ét sted: en pull request har en klikbar URL, og nogen har sat de samme nøgler på den som på produktion, 'så det virker'. Stripe live. Database med rigtige kunder. Webhook-secret fra butikken. En cron der rører produktion. Preview-indlægget er hvorfor I overhovedet vil have en URL på branchen. Staging-indlægget er hvornår I skylder en kopi, der ligner produktion. Den her er nøglerne. En preview, der kan det samme som produktion, er ikke et miljø. Det er et læk, I kalder convenience.

En preview-URL er delt: i PR'en, i Slack, i en mail til kunden, nogle gange indeksérbar hvis I glemmer robots. Den er ikke et VPN. Den er ikke 'kun til os'. Hvis den kan debitere, læse persondata eller kalde det webhook, I bruger i butikken, har I givet den person, der åbnede linket, det I troede I havde låst inde bag deploy-knappen. Sikkerheds-indlægget er headers og admin på www. Her er pligten at lade preview være et andet sæt hemmeligheder — eller at lade den være så fattig, at den ikke kan skade.

TL;DR

De vigtigste pointer

  • Preview er delt. Produktion er det, I skylder at holde hemmeligt. Samme nøgle i begge er et læk.
  • Test-nøgler, en kopi-database og et dummy-webhook slår 'det virker ligesom live' på en PR-URL.
  • Vercel, Cloudflare og GitHub Environments er mapper. De er ikke et skel, før I faktisk putter forskellige værdier i dem.
  • En cron, en migration eller et Stripe-live-kald på preview er hvordan I rammer rigtige kunder fra en branch.
  • Staging-indlægget er en kopi der ligner. Preview-deploys er URL'en på PR'en. Den her er at nægte at dele secrets på tværs.
  • I har skilt miljøerne, når I kan sige hvad preview ikke kan — ikke når I har tre URL'er med samme .env.

Hvorfor det her overhovedet er vigtigt

Et preview-deploy er et løfte om at nogen kan se branchen uden at køre den lokalt. Løftet rådner, når I fylder URL'en med det, produktion bruger til at tage imod penge, gemme kunder og lytte til webhooks. I har ikke gjort review lettere. I har flyttet produktionsadgangen ud på et link, I poster i en kommentar. Den, I inviterede til at 'lige tjekke knappen', kan nu det, I troede kun pipelinen kunne.

Teams gør det, fordi den første preview der fejler på manglende env, bliver mødt med 'sæt de samme som prod, så kommer vi videre'. Det virker. Det bliver vanen. Tre måneder senere har hver PR Stripe live, en connection string til den rigtige base, og et webhook-secret I rullede sidste år. I har ikke et preview-miljø. I har produktion med en anden hostname og uden den gate, I satte på main.

Skaden er konkret. En test-checkout på preview debiterer et rigtigt kort. En seed-script tømmer eller skriver i den rigtige base. Et webhook fra Stripe rammer både prod og den preview, I glemte, fordi I pegede endpointet det forkerte sted. En AI-nøgle på en offentlig preview bliver scrapet. I opdager det, når nogen spørger hvorfor der står to betalinger, eller hvorfor en branch 'lige skulle bruge prod-data'. I retter den ene PR. I rører ikke mappen.

Vi skriver det, fordi 'env er sat' er blevet et tjek, I sætter når buildet er grønt. Et ærligt setup er kedeligt: preview har test-nøgler og en base, I må ødelægge. Produktion har de rigtige, og kun pipelinen til main (eller den host, I faktisk ejer) kan læse dem. Uden det skel har I en .env, I kopierede. I har ikke miljøer.

Hvad et preview-secret faktisk er — og hvad en env-mappe ikke er

Et secret er en værdi, der giver adgang til noget, I ikke vil have ude. En API-nøgle, en database-URL, et webhook-secret, en signing key, en service-account. Preview-secret er den variant, I er villige til at lade følge en branch-URL. Produktions-secret er den variant, I kun vil lade den host læse, der tager imod rigtige brugere. Forskellen er ikke navnet på variablen. Forskellen er hvilken dør nøglen åbner.

Det er ikke det samme som 'vi har Preview, Preview, Production i Vercel'. Mapperne er hylder. Hvis I putter den samme værdi på alle tre, har I én hylde med tre labels. Cloudflare, Netlify, GitHub Environments, Doppler, Infisical — samme job. Et skel findes, når preview-værdien ikke kan det, prod-værdien kan. En `STRIPE_SECRET_KEY` der er `sk_live_…` på preview, er produktion. I har bare hængt den på en anden URL.

Det er heller ikke det samme som et staging-miljø. Staging-indlægget er en længelevende kopi, der skal ligne produktion i form: samme migrationer, samme jobs, ofte anonymiseret data. Preview er kortlivet og PR-bundet. De kan dele princippet — egne nøgler — men de er ikke samme job. At kalde preview for staging, fordi URL'en 'ligner', er hvordan I giver en feature-branch adgang til den base, I bruger til UAT med rigtige kunder.

Og det er ikke 'NEXT_PUBLIC_ er fint, det er jo bare public'. Det der er public i bundle, er public på preview og på prod. En publishable Stripe-nøgle er designet til det. En service-rolle, en intern API-token, en database-connection i en client-component er det ikke. Preview gør lækket lettere at finde, fordi URL'en er i en PR, I deler. Byg den vane, at det der lander i klienten, må stå i et screenshot. Resten lever kun på serveren, og kun i det miljø, I mente.

Det der typisk går galt, når nogen siger env er sat

Den første fejl er at kopiere produktion 'så preview ligner'. Checkout virker. Webhooks virker. Cron virker. I har bevist at branchen kan det, produktion kan — med produktionens nøgler. Det er ikke et review. Det er et ekstra produktionsvindue. Retningen er den omvendte: preview skal ligne nok til at I kan se UI og de stier, I kan drive med test-nøgler. Det, I ikke kan drive uden live, slår I fra eller peger på en sandbox.

Den anden fejl er at lade preview kalde rigtige side-effekter. En 'send kvittering'-knap der rammer Resend/Postmark med prod-domænet. En 'kør migration' i en preview-build. En synk til ERP. En Slack-incoming, I bruger til alarms. Hver side-effekt, preview kan udløse, er en side-effekt I har givet til den, der har URL'en. Staging kan have en kontrolleret synk. Preview skal som udgangspunkt ikke have nogen.

Den tredje fejl er at tro at 'Protected' på Vercel eller environment reviewers på GitHub er skellet. Det er en port foran hvem der kan læse eller deploye. Det er godt. Det erstatter ikke forskellige værdier. En beskyttet mappe med prod-nøgler, som I også har sat på Preview så 'designeren kan teste betaling', er stadig et læk. Porten var til main. I åbnede den for en branch, fordi demoen skulle virke.

Den fjerde fejl er secrets i repoet, i preview-logs, eller i client-bundle, I kaldte server. En `.env` I committede 'kun til preview'. En debug-side der printer `process.env`. En server action I sendte til klienten. Preview-logs er oftere åbne for flere øjne end prod-logs. Skriv vanen: nøgler lever i hostens mappe, ikke i git; logs må ikke spejle dem; det der er `NEXT_PUBLIC_` er et screenshot. Sikkerheds-indlægget er headers på www. Her er at nægte at lade PR-URL'en være den der lækker resten.

Egne preview-nøgler vs. samme .env overalt

Det ærlige skel

Preview åbner test-døre. Produktion åbner de rigtige. I kan pege på hvad preview ikke kan.

  • Test-nøgler, sandbox, en base I må ødelægge — eller integrationen slået fra på preview
  • Ingen cron, ingen live-webhook, ingen mail til rigtige kunder fra en PR-URL
  • Mapper i Vercel/Cloudflare/GitHub med forskellige værdier, ikke kun forskellige labels
  • Er kedeligt at vedligeholde, og det er hele pointen — en branch kan ikke debitere

Det kopierede læk

Tre URL'er, én .env, og en PR der kan det, main kan.

  • Stripe live, rigtig database, webhook fra butikken — 'så designeren kan klikke hele vejen'
  • Cron og migrationer kører på preview, fordi I genbrugte den samme job-definition
  • Protected environments, men samme værdi bag porten — porten var teater
  • Er et håb, I kalder miljø. Den, I sendte preview-linket til, har nøglerne i praksis
.env.preview-vs-productionbash
# Preview / pull request — døre I må ødelægge
STRIPE_SECRET_KEY=sk_test_preview_only
DATABASE_URL=postgres://preview-copy.invalid/app
WEBHOOK_SECRET=preview-dummy
CRON_ENABLED=false

# Production — døre I skylder at holde lukkede
STRIPE_SECRET_KEY=sk_live_only_on_production_host
DATABASE_URL=postgres://production.invalid/app
WEBHOOK_SECRET=production-only
CRON_ENABLED=true

Sådan beslutter I om preview overhovedet må have en nøgle — og hvilken

Uden den her rækkefølge får I endnu en mappe, I kalder skel.

  1. 01

    List de side-effekter, preview slet ikke må kunne

    Debitere. Skrive i den rigtige kundebase. Sende mail til rigtige adresser. Kalde ERP. Rydde et bucket. Hvis I ikke kan skrive den liste, har I ikke et secret-problem. I har et 'nogen sagde det skulle virke'-problem. Alt på listen er enten slået fra på preview, eller peger på en sandbox I ejer.

  2. 02

    Giv preview sit eget sæt — eller giv det intet

    En test-nøgle slår en live-nøgle. En kopi-database slår prod-URL'en. Et dummy-webhook slår butikkens secret. Hvis I ikke har sandbox, så lad integrationen være mørk på preview og vis en ærlig 'ikke tilgængelig her'-tilstand. Det er review. Et live-kald 'kun denne gang' er hvordan vanen starter.

  3. 03

    Læg værdierne i den mappe, hosten faktisk skiller

    Vercel: Preview ≠ Production. Cloudflare: preview-alias ≠ production. GitHub Environments: review-gates på production, andre værdier på preview. Ét sted, I kan pege på. En .env i repoet, 'kun til preview', er et secret i git. En værdi I satte i begge mapper 'for at komme videre', er ikke et skel.

  4. 04

    Stop cron, migrationer og webhooks på PR-URL'en

    Jobs der kører fordi I deployede, skal spørge hvilket miljø de er i — og nægte at køre, når svaret er preview. Webhooks fra vendor skal pege på produktion, eller på et staging I ejer, ikke på `*.vercel.app` fra en branch. Cutover og relancér er andre indlæg. Her er at nægte at lade en PR være et endpoint, Stripe kender.

  5. 05

    Giv lækket et interval, ikke kun en go-live

    Når I tilføjer en integration, beslutter I preview-værdien samme dag. Når I rotator en nøgle, rotator I den ikke ved at kopiere prod tilbage til preview. Når en designer skal 'se checkout', får de test-mode eller en optagelse — ikke sk_live på en URL i Slack. CI-indlægget er pipelinen. Her er vanen i mappen.

“I har skilt miljøerne, når preview ikke kan nå det, produktion skylder at holde hemmeligt.”

Checkout, webhooks, cron og den delte demo — samme ord, fire jobs

Checkout er det job, der oftest får live-nøglen 'så kunden kan klikke'. Det er det job, der oftest debiterer to gange. Preview får test-mode, eller den får en skærm der siger at betaling ikke kører her. En designer, der skal se kvitteringssiden, får et fixture eller en staging, I ejer — ikke en PR med sk_live. Stripe, Quickpay, Reepay: sandbox findes, fordi det her job er rigtigt. Brug den.

Webhooks er det job, I glemmer, fordi endpointet 'bare skal pege et sted hen'. Vendor kalder den URL, I har sat i dashboardet. Hvis I sætter en preview-URL, dør endpointet med branchen — eller I lader en gammel preview stå, og den får events, produktion også får. Peg produktion på produktion. Peg sandbox på preview eller på et staging, I har tænkt. To endpoints. To secrets. Ét dashboard-felt, I ikke deler.

Cron og baggrundsjobs er det job, der oftest kører, fordi I genbrugte den samme definition. Et preview-deploy der 'bare starter appen' starter også den synk, I kun ville have om natten på prod. Sæt et flag. Læs miljøet. Nægt at køre. Staging kan have jobs, I har valgt. Preview skal som udgangspunkt sove. En branch, der sender en bunke mails fordi I testede 'reminders', er ikke et miljø. Det er et udbrud.

Den delte demo er det job, I kalder preview, når I mener staging. En URL I sender til bestyrelsen i tre uger, med rigtige (eller rigtigt udseende) data, er ikke en PR. Staging-indlægget er den kopi. Preview dør med branchen og skal tåle at blive slettet. Hvis demoen skal leve, så giv den sit eget miljø og sine egne nøgler — ikke prod, og ikke 'den her PR vi holder åben'. En evig preview med prod-secrets er produktion uden den gate, I satte på main.

Hvad I typisk bør gøre i den situation, I står i

Venstre er det, I typisk siger. Højre er det næste skridt, vi typisk anbefaler.

  • Situation

    Preview fejlede på manglende env, så I kopierede prod-nøglerne

    Næste skridt

    Slet dem fra preview. Sæt test-nøgler eller slå integrationen fra. Det grønne build var det forkerte mål

  • Situation

    Designeren skal se checkout, og test-mode 'ser forkert ud'

    Næste skridt

    Fixture, screenshot, eller staging I ejer. Ikke sk_live på en URL i Slack

  • Situation

    Stripe-dashboardet peger webhook på en *.vercel.app fra sidste sprint

    Næste skridt

    Peg produktion på produktion. Preview får sandbox eller intet. Et dødt alias er et læk, I glemte

  • Situation

    Cron kører på hver preview, fordi job-definitionen er den samme

    Næste skridt

    Læs miljøet. Nægt at køre. Preview er et review, ikke et nat-skift

  • Situation

    I har tre mapper i Vercel og den samme værdi i alle tre

    Næste skridt

    I har én hylde. Skil værdierne, eller slet de mapper I ikke mener

Tjekliste før I kalder det skilte miljøer

Hvis I mangler flere af punkterne, er sætningen env er sat — setup'et er en .env, I kopierede.

  • I kan pege på en preview-værdi der ikke åbner den dør, prod-værdien åbner — eller på en integration der er mørk på preview
  • Ingen cron, migration eller live-webhook kører fordi I deployede en branch
  • NEXT_PUBLIC_ og client-bundle må stå i et screenshot. Resten lever kun på serveren i det miljø, I mente
  • Mapperne hos hosten har forskellige værdier, ikke kun forskellige labels
  • En evig demo er staging med egne nøgler — ikke en PR I holder åben med prod-secrets
  • CI-indlægget er pipelinen. Staging-indlægget er kopien. Den her er at nægte at dele nøglerne

Spørgsmål vi får igen og igen

  • Må preview bruge de samme environment variables som produktion?

    Nej — ikke de værdier, der åbner rigtige døre. Navnene må gerne være de samme. Værdierne må ikke. En preview-URL er delt i en PR og i chat. Hvis den kan debitere, læse kunder eller kalde butikkens webhook, har I givet linket adgang til produktion. Sæt test-nøgler, en kopi I må ødelægge, eller slå integrationen fra. 'Så det virker' er hvordan lækket bliver vanen.

  • Er det her det samme som at have et staging-miljø?

    Nej. Staging-indlægget er en længelevende kopi, der skal ligne produktion i form — migrationer, jobs, ofte anonymiseret data. Preview er kortlivet og dør med branchen. Begge skal have egne nøgler. De er ikke samme job. At holde en PR åben i tre uger med prod-secrets, fordi 'det er vores staging', er produktion uden gate. Giv demoen et miljø. Giv branchen et preview.

  • Hvordan skiller vi secrets i Vercel eller Cloudflare?

    Med de mapper, hosten allerede har: Preview og Production, eller tilsvarende alias. Put forskellige værdier i dem. En Protected-port på production er godt — den erstatter ikke at preview har test-nøgler. GitHub Environments kan review-gate production-deploys. Porten er hvem der må. Skellet er hvad der står bag. Samme værdi bag to labels er én hylde.

  • Hvad gør vi, når sandbox ikke ligner live nok til et review?

    Så er reviewet af den sti ikke et preview-job. Brug et fixture, en optagelse, eller et staging I ejer med testdata. Kopiér ikke sk_live ind på PR-URL'en 'kun i dag'. I kopierer den igen i morgen. En ærlig 'betaling kører ikke på preview'-skærm slår et live-kald, I ikke kan forklare i audit-loggen. Kunden kan se UI. De behøver ikke at kunne debitere fra en branch.

  • Er NEXT_PUBLIC_ variabler et secret-problem?

    De er et public-problem. Det, I prefixer til klienten, lander i bundle på preview og på prod. En publishable nøgle er designet til det. En service-rolle, en intern token, en connection string er det ikke — og preview gør dem lettere at finde, fordi URL'en er delt. Sikkerheds-indlægget er headers på www. Her er vanen: det der er public, må stå i et screenshot. Resten kommer aldrig med til klienten.

Hvis preview kan det, produktion kan, og I kalder det convenience

Lad os skille test-nøgler fra det, I skylder at holde hemmeligt.

Et skel i mappen slår en .env, I kopierede så buildet blev grønt.

Hvorfor vi skrev den her

Det er sådan vi tænker — og det er det vi bygger.

Vores indsigter handler om det vi rent faktisk leverer. Hvis det her ramte noget du arbejder med, så er der en konkret service der ligger tæt på.