Hoppa till innehåll

Webbservrar och reverse proxy

Den här sidan förklarar vad som står mellan webbläsaren och din Express-applikation när den är driftsatt. Den innehåller:

  • vad en webbserver och en reverse proxy är, och varför du behöver en
  • vad en reverse proxy lägger till i förfrågan, och vad det betyder för din applikation
  • hur förfrågningar styrs till rätt applikation
  • hur HTTPS, certifikat och certifikatutfärdare hänger ihop
  • grunderna i Caddy, som är kursens reverse proxy
  • övningar att göra på din egen dator

Allt du gör här gör du sedan på riktigt på din server, i guiden Din server på Cumulus.

Tidsåtgång: Ungefär 20–30 minuter att läsa och en timme för övningarna.

Du behöver Docker Desktop och en Bash-terminal, som i Docker-kommandon. Den sista övningen bygger på din lösning av övningen Containerisera en webbapplikation.

En webbserver tar emot HTTP-förfrågningar och skickar svar. Den kan skicka filer direkt från disken, som HTML, CSS och bilder, men den kan också skicka förfrågan vidare till ett program som skapar svaret. De vanligaste är nginx och Apache. I kursen använder du Caddy, som skaffar och förnyar certifikat för HTTPS helt av sig själv och har en kort konfiguration.

Din Express-applikation är också en webbserver, men en du inte vill släppa direkt mot internet. Den ska göra en sak, applikationens logik, och låta en beprövad webbserver sköta resten.

En reverse proxy står framför en eller flera applikationer och tar emot alla förfrågningar i deras ställe. Klienten pratar bara med proxyn och vet inte vilken applikation som till sist svarar.

webbläsare ──HTTPS──▶ reverse proxy ──HTTP──┬──▶ applikation A
(port 443) └──▶ applikation B

En vanlig proxy, en forward proxy, gör tvärtom: den står framför klienterna, till exempel på en arbetsplats, och gör förfrågningar åt dem ut mot internet.

Det här får du av en reverse proxy:

  • En enda ingång. Bara proxyn lyssnar på port 80 och 443. Många applikationer kan dela samma server och samma DNS-namn.
  • HTTPS på ett ställe. Proxyn har certifikatet och sköter krypteringen. Applikationerna bakom talar vanlig HTTP och behöver inte veta något om certifikat.
  • Applikationerna syns inte utåt. De publicerar inga portar och kan bara nås via proxyn.
  • Mer på köpet. Omdirigering från HTTP till HTTPS, komprimering, loggning och, om det behövs, lastbalansering mellan flera instanser av samma applikation.

Applikationen får förfrågan från proxyn, inte från webbläsaren. För att applikationen ändå ska veta vem som frågade och hur, lägger proxyn till headers:

Header Innehåll
X-Forwarded-For Klientens IP-adress
X-Forwarded-Proto https eller http, alltså hur klienten anslöt till proxyn
X-Forwarded-Host Värdnamnet klienten använde

Express litar inte på de här headers från början, eftersom vem som helst kan skicka dem. Står applikationen bakom en proxy talar du om det:

app.set('trust proxy', 1)

Då ger req.ip klientens adress i stället för proxyns, och req.protocol blir https. Det behövs bland annat för att cookies med flaggan secure ska skickas.

Styra förfrågningar till rätt applikation

Section titled “Styra förfrågningar till rätt applikation”

En reverse proxy kan välja applikation på två sätt:

Sätt Exempel Kräver
Värdnamn api.example.com, app.example.com Ett DNS-namn per applikation
Sökväg example.com/api/, example.com/app/ Bara ett DNS-namn

Din server på Cumulus har ett enda DNS-namn, så där styr du med sökväg.

Med sökväg måste du bestämma om prefixet ska tas bort innan förfrågan skickas vidare. I Caddy gör handle_path /app/* det, så applikationen får / när klienten frågade efter /app/. handle låter sökvägen vara, så applikationen får hela /app/.

Tas prefixet bort vet applikationen inte att den ligger under /app/. Länkar som börjar med /, som <a href="/login"> eller <link href="/css/style.css">, pekar då förbi prefixet och ger 404. Använd relativa länkar, eller låt applikationen få veta sin bas-URL, till exempel i en miljövariabel.

HTTPS är HTTP i en krypterad TLS-anslutning. För att webbläsaren ska veta att den pratar med rätt server visar servern ett certifikat: ett intyg om att en viss nyckel hör till ett visst DNS-namn, signerat av en certifikatutfärdare (certificate authority, CA). Webbläsaren och operativsystemet har en lista med CA:er de litar på. Är certifikatet signerat av en av dem syns ett hänglås, annars en varning.

Certifikat hämtas och förnyas automatiskt med protokollet ACME. Servern bevisar för CA:n att den kontrollerar DNS-namnet och får ett certifikat tillbaka.

CA Används Litar webbläsaren på den?
Let’s Encrypt Publika webbplatser Ja, från början
LNU FTK Campus CA Din server på Cumulus Nej, du importerar rotcertifikatet en gång
Caddys lokala CA Utveckling på din egen dator Nej, men du kan importera det

Certifikat har kort livslängd, några dagar till några månader, och förnyas därför automatiskt. Det är just det Caddy gör åt dig.

Caddy konfigureras i filen Caddyfile. Varje block börjar med en adress som anger vilka förfrågningar blocket gäller, och innehåller direktiv som anger vad som ska hända:

example.com {
handle_path /app/* {
reverse_proxy app:3000
}
respond "Ingen app här" 404
}

Indragen ska vara tabbar.

Adress Betydelse
example.com HTTPS för example.com, med certifikat som Caddy hämtar själv. HTTP omdirigeras till HTTPS.
localhost HTTPS med ett certifikat från Caddys lokala CA
:80 Vanlig HTTP på port 80, för alla värdnamn
Direktiv Vad det gör
reverse_proxy app:3000 Skickar förfrågan vidare till app på port 3000
handle_path /app/* { … } Förfrågningar under /app/, med prefixet borttaget
handle /app/* { … } Förfrågningar under /app/, med sökvägen orörd
redir /app /app/ Omdirigerar till en annan adress
respond "text" 404 Svarar direkt med en text och en statuskod
file_server Skickar filer från disken
import sites/*.caddy Läser in fler filer, till exempel en per applikation

Ändrar du Caddyfile läser Caddy in den på nytt med caddy reload, utan att avbryta pågående förfrågningar. Kör Caddy i en container gör du det med docker exec -w /etc/caddy <container> caddy reload.

Tidsåtgång: Ungefär en timme för alla sju övningarna.

Övningarna görs på din egen dator. Gör dem i ordning och i samma katalog. Försök själv innan du tittar på lösningsförslaget. Kör Git Bash på Windows? Se till att du har lagt till funktionen docker() från Docker-kommandon.

Som applikation används traefik/whoami:v1.11, en liten webbserver som svarar med den förfrågan den tog emot. Då ser du exakt vad proxyn skickar vidare.

Tidsåtgång: Ungefär 5 minuter.

Du kör Caddy i en container och styr den med en konfigurationsfil som du skriver på din egen dator. Filen lämnar du över till containern när du startar den.

  1. Skapa katalogen caddy i din övningskatalog, och i den en fil med namnet Caddyfile. Filen ska få Caddy att svara Hej från Caddy på alla förfrågningar på port 80.
  2. Starta en container från imagen caddy:2 och ge den namnet proxy. När du startar den:
    • montera katalogen caddy på /etc/caddy i containern, som en bind mount. Caddy läser sin konfiguration från /etc/caddy/Caddyfile, så din fil ersätter den som följer med imagen.
    • publicera containerns port 80 på port 8080 på 127.0.0.1.
  3. Öppna localhost:8080.

caddy/Caddyfile:

:80 {
respond "Hej från Caddy"
}
Terminal window
docker run -d --rm --name proxy -p 127.0.0.1:8080:80 -v "./caddy:/etc/caddy:ro" caddy:2

Tidsåtgång: Ungefär 10 minuter.

Nu lägger du en applikation bakom Caddy. Applikationen publicerar ingen egen port, utan nås bara via Caddy över ett gemensamt nätverk.

  1. Skapa nätverket rp.
  2. Starta en container från imagen traefik/whoami:v1.11 med namnet app på nätverket rp. Publicera ingen port.
  3. Ändra din Caddyfile så att Caddy skickar alla förfrågningar vidare till app på port 80.
  4. Stoppa containern proxy och starta den igen, som i övning 1 men på nätverket rp.
  5. Öppna localhost:8080/hej. Vilka headers har Caddy lagt till, och vilken IP-adress står i X-Forwarded-For?

caddy/Caddyfile:

:80 {
reverse_proxy app:80
}
Terminal window
docker network create rp
docker run -d --rm --name app --network rp traefik/whoami:v1.11
docker rm -f proxy
docker run -d --rm --name proxy --network rp -p 127.0.0.1:8080:80 -v "./caddy:/etc/caddy:ro" caddy:2

Caddy har lagt till X-Forwarded-For, X-Forwarded-Host och X-Forwarded-Proto, och Host är det värdnamn du skrev i webbläsaren. I X-Forwarded-For står adressen som din förfrågan kom från sett inifrån Docker, inte 127.0.0.1. RemoteAddr är Caddys adress på nätverket rp, eftersom det är Caddy som ansluter till app.

app kan inte nås direkt från din dator. Caddy hittar den på namnet, eftersom de är på samma nätverk.

3. Två applikationer på olika sökvägar

Section titled “3. Två applikationer på olika sökvägar”

Tidsåtgång: Ungefär 10 minuter.

Nu lägger du två applikationer bakom samma Caddy och låter sökvägen avgöra vilken som får förfrågan. Den här gången läser Caddy in den ändrade Caddyfile utan att containern startas om.

  1. Starta en container till från imagen traefik/whoami:v1.11 med namnet app2 på nätverket rp. Publicera ingen port.
  2. Ändra din Caddyfile så att:
    • förfrågningar under /a/ går till app med handle_path
    • förfrågningar under /b/ går till app2 med handle
    • allt annat får svaret Ingen app här med status 404.
  3. Läs in den ändrade Caddyfile i den körande containern proxy, utan att starta om den.
  4. Öppna localhost:8080/a/x och localhost:8080/b/x. Vilken sökväg får applikationerna?
  5. Öppna localhost:8080/a, utan snedstreck sist. Vad händer?

caddy/Caddyfile:

:80 {
handle_path /a/* {
reverse_proxy app:80
}
handle /b/* {
reverse_proxy app2:80
}
respond "Ingen app här" 404
}
Terminal window
docker run -d --rm --name app2 --network rp traefik/whoami:v1.11
docker exec -w /etc/caddy proxy caddy reload

app får GET /x, eftersom handle_path tar bort /a. app2 får GET /b/x. /a utan snedstreck matchar inte /a/* och ger 404. Lägg till redir /a /a/ för att skicka den rätt.

Tidsåtgång: Ungefär 5 minuter.

Stoppa app2 och öppna /b/x igen. Vilken statuskod får du, och vad står i Caddys logg?

Terminal window
docker stop app2
docker logs proxy

502 Bad Gateway: Caddy fick förfrågan men kunde inte nå applikationen bakom. Loggen säger att namnet app2 inte gick att slå upp. Får du 502 på din server är det första du kontrollerar alltså att applikationen är igång och att namnet och porten i reverse_proxy stämmer.

Tidsåtgång: Ungefär 10 minuter.

Ändra adressen i Caddyfile till localhost och låt den skicka allt till app. Starta om Caddy och koppla även port 8443 på 127.0.0.1 till port 443. Öppna https://localhost:8443. Varför varnar webbläsaren? Vem har utfärdat certifikatet, och hur länge gäller det? Vad står i X-Forwarded-Proto?

caddy/Caddyfile:

localhost {
reverse_proxy app:80
}
Terminal window
docker rm -f proxy
docker run -d --rm --name proxy --network rp -p 127.0.0.1:8080:80 -p 127.0.0.1:8443:443 -v "./caddy:/etc/caddy:ro" caddy:2

Certifikatet är utfärdat av Caddy Local Authority, en CA som Caddy skapade när den startade. Din webbläsare litar inte på den, därav varningen. Certifikatet gäller bara i tolv timmar. Klicka på hänglåset eller varningen i webbläsaren för att se detaljerna. X-Forwarded-Proto är nu https.

Du kan lita på CA:n genom att hämta dess rotcertifikat, precis som du gör med Cumulus CA på din server:

Terminal window
docker cp proxy:/data/caddy/pki/authorities/local/root.crt caddy-root.crt
curl --ssl-no-revoke --cacert caddy-root.crt https://localhost:8443

--ssl-no-revoke behövs i Git Bash på Windows, där curl annars försöker kontrollera en spärrlista som den här CA:n inte har.

Tidsåtgång: Ungefär 15–20 minuter.

Nu ska du köra din lösning på övningen Containerisera en webbapplikation på samma sätt som på servern: Caddy i ett eget compose-projekt i katalogen proxy, med konfigurationen i proxy/caddy och en fil per applikation i proxy/caddy/sites, och applikationen med compose.production.yaml på nätverket proxy. Städa först bort containrarna och nätverket från övningarna ovan. Gör så att localhost:8080/hello-world/ visar applikationen.

Terminal window
docker rm -f proxy app
docker network rm rp
docker network create proxy
mkdir -p proxy/caddy/sites

proxy/compose.yaml:

services:
caddy:
image: caddy:2
restart: unless-stopped
ports:
- 127.0.0.1:8080:80
volumes:
- ./caddy:/etc/caddy:ro
networks:
- proxy
networks:
proxy:
external: true

proxy/caddy/Caddyfile:

:80 {
import sites/*.caddy
respond "Caddy fungerar"
}

proxy/caddy/sites/hello-world.caddy:

redir /hello-world /hello-world/
handle_path /hello-world/* {
reverse_proxy hello-world:3000
}
Terminal window
cd proxy
docker compose up -d
cd <katalogen med din lösning>
docker compose -f compose.production.yaml up -d --build

Applikationen ansluter till nätverket proxy med namnet hello-world och publicerar ingen port. Det enda som skiljer mot servern är adressen i Caddyfile, där servern har sitt DNS-namn och får ett certifikat från Cumulus CA.

Tidsåtgång: Ungefär 2 minuter.

Stoppa och ta bort applikationen, Caddy och nätverket proxy.

Terminal window
docker compose -f compose.production.yaml down # i katalogen med din lösning
docker compose down # i katalogen proxy
docker network rm proxy
CCBY