Generelt
- Meld deg på e-postlisten sympa.uio.no/usit.uio.no/info/w3d-lb for nyheter og viktig informasjon om lastbalansering på UiO.
Vi anbefaler alltid at trafikk i første rekke håndteres av en fullverdig webserver, slik som Apache eller nginx. Selv om det ofte i prinsippet er mulig å la lastbalanserer snakke direkte med en applikasjonsserver, mister man da mye fleksibilitet og mange muligheter. Hvis man trenger å håndtere sertifikater på backend er som regel dette også langt enklere å gjøre i en webserver, og man kan simpelthen følge oppskriftene for hhv. Apache eller nginx, som er dokumentert i kokeboken for SSL.
Vi anbefaler videre at webserveren settes til å lytte på port 80 og/eller 443 (istedenfor ustandard porter), og begrenses til å kun tillate trafikk mot disse portene fra UiOs nett. Dette kan man oppnå helst ved å plassere (eller flytte om nødvendig) serveren til et VLAN tilhørende et mer lukket nett, f.eks. kategori 3, eller sekundært ved å begrense innkommende trafikk i lokal firewall når dette ikke er mulig. Hovedformålet er altså å forhindre at webserver kan ta imot trafikk direkte fra verden, og dermed tvinge innkommende trafikk gjennom lastbalanserer.
Dersom webserver reversproxyer videre til en applikasjonsserver over HTTP, kan denne egentlig benytte hvilken som helst port over 1024. Ofte benyttes port 8080 til dette formålet for lokal ukryptert trafikk. Hvis applikasjonsserver kjører på samme server som webserver - hvilket den som regel gjør - bør applikasjonsserveren kun lytte på/binde mot localhost, og ikke public IP. Webserver bør da følgelig også reversproxye videre til localhost, over valgt port, f.eks. 8080. Hovedformålet med dette er igjen å forhindre at applikasjonsserver kan ta imot trafikk utenfra - inkludert fra andre servere - og dermed tvinge innkommende trafikk til denne gjennom webserver. Hvis applikasjonsserver kjører på en annen server enn webserver, bør applikasjonsserver kun tillate trafikk fra IPen(e) til webserver, begrenset med lokal firewall.
Gjør man alt dette, sørger man altså for å stenge bakdører, og at all innkommende web-trafikk vil følge ønsket vei: lastbalanserer -> webserver -> applikasjonsserver.
Applikasjonsservere skal kjøres som en dedikert upersonlig bruker, og aldri som root. Som regel vil en lokal bruker være tilstrekkelig. Webservere (og applikasjonsservere der dette er mulig) må alltid installeres med systempakke fra standard-repo, som da vil benytte dedikert lokal bruker og sørge for at egnede rettigheter ivaretas.
Gjenopprette korrekt IP
Når HTTP-trafikk går gjennom en reversproxy, slik den gjør ved den typen lastbalansering vi gjør (lastbalansering i HTTP-laget), gjøres det i realiteten to separate forespørsler. En fra klient til lastbalanserer, og så en ny fra lastbalanserer til webserver (backend). Det er dette som gjør det mulig å påvirke egenskaper ved forespørselen i lastbalanseringslaget, og "fikse" ting der, før den går videre til webserver. Ettersom disse forespørslene er separate, kan de også ha forskjellige egenskaper. Forespørselen fra klient til lastbalanserer kan f.eks. gå over IPv6 og HTTP/2, mens forespørselen fra lastbalanserer til backend kan gå over IPv4 og HTTP/1.1 - eller andre kombinasjoner av faktorer. Lastbalanserer vil oversette mellom disse, så sømløst som mulig.
En negativ konsekvens ved dette, er at forespørslene fra lastbalanserer til backend ser ut i webserver som de kommer fra IPen som tilhører lastbalansereren, istedenfor IPen som tilhører klienten. Dette stemmer jo for så vidt, men man er som regel mer interessert i hvilken klient som faktisk sitter i andre enden. Denne informasjonen (og mer til) eksponerer lastbalanserer gjennom egne HTTP headere, særlig headerne X-Forwarded-For og Forwarded. Begge disse gjør i prinsippet det samme, men X-Forwarded-For er mer utbredt, og ofte enklere å ta i bruk. Vha. disse headerne kan man i webserver "gjenopprette" den opprinnelige IPen som faktisk tilhører klienten, og dermed også eksponere denne videre inn i tjenesten til selve applikasjonen. Støtte for å gjøre dette er høyst varierende i ymse applikasjonsservere, og dette er én av grunnene til at vi alltid anbefaler å ha en skikkelig webserver - med støtte for dette og langt mer - i forkant. Da vil man ha tilgang til det fulle og rike verktøyskrinet av muligheter som en webserver tilbyr, og få logger med korrekt IP fra webserveren.
Det er mange grunner til at det er viktig å sette opp webserver til å korrekt rapportere klientens IP-adresse. Uten dette vil det være vanskelig (eller sågar umulig) å skille forskjellige klienter fra hverandre, både i logger og ellers. Logger som rapporteres inn i DataOPS vil være langt mindre nyttige eller verdifulle, ettersom de mangler en av de mest vesentlige detaljene. Det vil være utfordrende å debugge problemer med tjenesten, og dersom det skulle oppstå en hendelse vil denne være vanskelig for IT-sikkerhet å kunne analysere. I det store og hele vil man i de fleste sammenhenger være mer eller mindre blind mht. hvem som faktisk bruker tjenesten.
Videresending fra HTTP til HTTPS
Det er i dag et lovkrav i offentlig forvaltning å videresende forespørsler mot HTTP direkte videre til HTTPS. Lastbalanserer vil alltid gjøre dette, automatisk og ubetinget, slik at alle tjenester satt opp med lastbalansering vil oppfylle dette kravet. Dersom man har logikk på backend som er ment å håndtere dette, kan (og bør) denne med andre ord skrus av, ettersom dette ellers kan skape problemer.
Selv om man unngår å gjøre dette i webserver, hender det at enkelte applikasjoner har logikk for å ta seg av slik videresending, eller vurdere hvilken protokoll innkommende forespørsler er gjort over. Hvis tjenesten er satt opp med HTTP-trafikk (ikke HTTPS) mellom lastbalanserer og backend, kan dette i noen tilfeller skape problemer, dersom applikasjonen tror forespørsler er gjort over HTTP (som de jo er for forespørslene den "ser"), når de i realiteten er gjort over HTTPS mellom klient og lastbalanserer, som er det vesentlige. Da kan man risikere å få en uendelig loop, ettersom alle forespørsler vil resultere i en videresending til HTTPS-adressen, som klienten allerede bruker.
Løsningen på dette er å ta i bruk en HTTP header som heter X-Forwarded-Proto, eller evt. proto-verdien i Forwarded-headeren dersom man istedet bruker denne. Denne forteller hvilken protokoll den opprinnelige forespørselen benyttet, og settes av lastbalanserer for alle innkommende forespørsler. Ved å få applikasjon (eller webserver, avhengig av behov og oppsett) til å anse verdien av denne headeren som faktisk benyttet protokoll, istedenfor protokollen i forespørselen den selv ser, omgår man altså dette problemet.
OpenShift/OKD
Alle tjenester som kjører i OpenShift/OKD må plasseres bak lastbalanserer - særlig om de skal være tilgjengelig fra Internet. Her anbefales det først og fremst å følge den generelle OpenShift-dokumentasjonen, med særlig fokus på seksjonene for hhv. Edge og Passthrough.
Merk at de generelle anbefalingene for oppsett bak lastbalansering gjelder også her, og da særlig det som gjelder å ha en webserver foran der dette er mulig, anbefalt oppsett for webservere som nginx og Apache, og håndtering av tilgangskontroll.
Det kjører en HTTP Echo-tjeneste i OpenShift som vil vise hvilke headere som er tilgjengelig fra innsiden av et cluster, og som man kan benytte seg av. Legg særlig merke til headerne Forwarded samt X-Forwarded-* som alle stammer fra lastbalansererne.
Apache
Apache HTTP server er nok den web-serveren vi har mest erfaring med, særlig i www-drift. I utgangspunktet trenger man ikke å gjøre noen endringer i Apache-oppsettet for å få dette til å virke med våre lastbalanserere, men endringer er nødvendig for å gjenopprette opprinnelig klient-IP. For å gjøre dette i Apache, benytter man modulen mod_remoteip, hvor dette kan løses med kun noen få linjer konfigurasjon.
mod_remoteip
Først må man skru på modulen mod_remoteip. På Red Hat er denne allerede inkludert i httpd-pakken, så man trenger ikke installere noe ekstra for dette. Hvis modulen ikke allerede er lastet inn, kan den enkelt legges inn som en linje der man laster inn andre moduler:
Loadmodule remoteip_module modules/mod_remoteip.so
Videre må man fortelle Apache hvilken header den skal hente IP fra, samt hvilke lastbalanserere man har i forkant.
RemoteIPHeader X-Forwarded-For RemoteIPTrustedProxy w3prod-lb-uio01.uio.no RemoteIPTrustedProxy w3prod-lb-uio02.uio.no
Noen vil kanskje foretrekke å heller bruke IPv4- og IPv6-adressene. Da må man i så fall huske å legge inn begge, altså både IPv4 og IPv6 for begge lastbalansererne, siden trafikk kan komme fra begge lastbalanserere og begge IP-versjoner.
I eksempelet over sier man altså at klientens IP-adresse skal hentes fra HTTP-headeren X-Forwarded-For, og at denne informasjonen kun skal stoles på dersom den stammer fra lastbalansererne w3prod-lb-uio01.uio.no eller w3prod-lb-uio02.uio.no.
I noen tilfeller vil tjenesten settes opp på andre lastbalanserere enn de i eksempelet, f.eks. dersom tjenesten er en test-instans, eller dersom den kjører i et annet miljø (OpenShift, Educloud eller annet). Det ligger en full liste over alle lastbalanserere i git, men er du usikker på dette så spør oss gjerne. Det er også mulig å si at man stoler på hele UiOs nett, men man er da nødt til å ta høyde for både IPv4 og IPv6. Dette er i utgangspunktet noe mindre sikkert, og gjøres i så fall på eget ansvar.
Loggformat
Loggformatet må så oppdateres til å benytte verdien fra "X-Forwarded-For". Man kan benytte hvilket loggformat man måtte ønske, men vi vil foreslå flg., ettersom dette enkelt vil kunne parses av DataOPS uten behov for videre tilpasninger:
LogFormat "%a %v %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %D" combinedDet siste ordet i linjen er navnet på loggformatet man har definert, som man så må sørge for å gjenbruke der man definerer logger. I dette eksempelet defineres altså et loggformat kalt "combined". Dette er et navn som er ofte brukt i Apache, og gjerne vil være definert fra før av, men da med et annet format enn det vi her foreslår.
Det enkleste vil da være å finne linjene som starter på CustomLog, og se på slutten av linjen for å finne ut navnet på loggformatet man allerede bruker, f.eks. "combined" eller "common". Man kan så finne den eksisterende LogFormat-linjen som slutter på dette navnet, og bytte denne ut med linjen over, med samme navn på slutten som det man alt bruker i CustomLog-linjer. Da holder det å oppdatere denne ene linjen (som starter på LogFormat), og man trenger ikke å endre noen linjer som definerer logger (de som starter på CustomLog).
Alternativt kan man legge til en ny loggformat-definisjon lik den over, og istedenfor "combined" kalle den f.eks. "uio" eller hva som helst. Da kan man spesifisere at logger istedet skal bruke dette nye formatet kalt "uio". Et eksempel på sistnevnte kunne sett slik ut:
CustomLog /var/log/httpd/foo.uio.no/access_log uio
Om man ikke ønsker å bruke det foreslåtte loggformatet, kan man gjøre en mindre endring i det loggformatet man selv foretrekker, og simpelthen bytte fra "%h" til "%a". Dette er i seg selv tilstrekkelig for å få riktig kilde-IP i logger.
HTTP/2
Man bør også sørge for å skru på støtte for HTTP/2, ettersom denne har langt bedre ytelse enn den eldre (og nå ganske gamle) HTTP/1.1. I webservere - også Apache - må dette vanligvis aktivt skrus på for å brukes.
Dette gjør man ved å først installere pakken mod_http2 med flg. systemkommando:
sudo yum install -y mod_http2
Så laste modulen i Apache-config, samme sted hvor man laster andre moduler:
LoadModule http2_module modules/mod_http2.so
Og til sist aktivere protokollen i Apache-config:
Protocols h2 h2c http/1.1
Denne siste linjen kan erstatte evt. tidligere Protocols-linje(r) man måtte ha fra før. Hvis man ikke har en slik fra før av, kan denne puttes enten (helst) i generell server config, eller (evt.) inni en VirtualHost-blokk.
Når trafikk fra lastbalanserer til backend (altså Apache) går over HTTPS (port 443), vil da oppgradering til HTTP/2 skje automatisk, og man trenger ikke foreta seg noe mer.
Hvis den derimot går over HTTP (port 80), vil ikke oppgradering til HTTP/2 skje av seg selv, og lastbalanserer må eksplisitt fortelles at den skal bruke dette. Dette gjør man ved å be www-drift om å legge dette til. Konfigurasjonen man skal be om er dette:
backend_http_version: proto h2
Komprimering
Komprimering av innhold før det sendes til klient vil forbedre ytelsen på lasting av sider, og dermed brukeropplevelsen. Her er det dog flere hensyn å ta. Ikke alle filtyper er komprimerbare, f.eks. kan man normalt ikke komprimere bilder eller annet mediainnhold. Det finnes dessuten forskjellige komprimeringsalgoritmer å velge mellom, og ikke alle er støttet av alle servere, eller alle klienter. Det riktige valget vil være den beste (og som regel mest moderne) komprimeringsalgoritmen som er støttet av backend, som klient oppgir at den støtter (i request headeren Accept-Encoding).
Når ikke backend komprimerer innhold, vil lastbalanserer automatisk komprimere svaret fra en backend dersom dette ikke alt er komprimert, så fremt filtypen er av en type den vet kan komprimeres. Dette sørger for at komprimering alltid tas i bruk når dette gir mening, for all trafikk som går gjennom lastbalanserer. Likevel er det ønskelig at backend komprimerer innhold der dette er mulig, av flere årsaker:
- Når komprimeringen gjøres av backend istedenfor lastbalanserer, sørger man for at lasten av å komprimere fordeles utover alle tjenester, istedenfor at lastbalanserer må påta seg hele byrden for alle
- Lastbalanserer støtter per i dag ikke de mest moderne komprimeringsalgoritmene, som gjerne er støttet i webservere, og vi har ikke ennå funnet noen måte å integrere dette på
- Lastbalanserer har en øvre antallsbegrensning i listen over filtyper man kan angi at ønskes komprimert
For Apache kan dette enklest løses ved å ta i bruk samme konfigurasjon som www-drift benytter på sine servere. Denne dekker alle relevante filtyper, og sørger for at den mest effektive komprimeringsalgoritmen som støttes av begge parter til enhver tid blir brukt. Filen kan plasseres f.eks. i /etc/httpd/conf.d/, og dette vil som regel være alt du trenger å gjøre.
Dersom du ikke har tilgang til filen, ta kontakt med www-drift, så kan vi enten gi deg tilgang eller sende deg en kopi.
nginx
I utgangspunktet trenger man ikke å gjøre noen endringer i nginx-oppsettet for å få dette til å virke med våre lastbalanserere, men endringer er nødvendig for å gjenopprette opprinnelig klient-IP. For å gjøre dette i nginx, benytter man modulen real_ip, hvor dette kan løses med kun noen få linjer konfigurasjon. På Red Hat er denne allerede inkludert i nginx-pakken, så man trenger ikke installere noe ekstra for dette.
real_ip
For å ta i bruk real_ip, legger man til følgende i "http"-seksjonen av konfigurasjon, der "..." både før og etter er resten av den eksisterende konfigurasjonen. Den relevante koden må legges til før "log_format" defineres:
http {
...
real_ip_header X-Forwarded-For;
real_ip_recursive on;
set_real_ip_from w3prod-lb-uio01.uio.no;
set_real_ip_from w3prod-lb-uio02.uio.no;
...
}
I eksempelet over sier man altså at klientens IP-adresse ("Real IP") skal hentes fra HTTP-headeren X-Forwarded-For, og at denne informasjonen kun skal stoles på dersom den stammer fra lastbalansererne w3prod-lb-uio01.uio.no eller w3prod-lb-uio02.uio.no. Siden versjon 1.13.1 av nginx skal det være mulig å angi hostnavn på denne måten, men hvis det ikke fungerer, kan du istedet legge inn IP-adressene til lastbalansererne. Det er da viktig at du legger inn både IPv4- og IPv6-adressene til begge lastbalansererne, altså totalt fire oppføringer.
I noen tilfeller vil tjenesten settes opp på andre lastbalanserere enn de i eksempelet, f.eks. dersom tjenesten er en test-instans, eller dersom den kjører i et annet miljø (OpenShift, Educloud eller annet). Det ligger en full liste over alle lastbalanserere i git, men er du usikker på dette så spør oss gjerne. Det er også mulig å si at man stoler på hele UiOs nett, men man er da nødt til å ta høyde for både IPv4 og IPv6. Dette er i utgangspunktet noe mindre sikkert, og gjøres i så fall på eget ansvar.
Loggformat
Når man bruker real_ip-modulen i nginx, vil denne sørge for å benytte korrekt IP også i logger. Det er dermed ikke nødvendig å gjøre noen ytterligere justeringer i nginx for å få riktige logger.
HTTP/2
I tillegg anbefaler vi at man tillater trafikk over HTTP/2, ettersom denne har langt bedre ytelse enn den eldre (og nå ganske gamle) HTTP/1.1. I webservere - også nginx - må dette vanligvis aktivt skrus på for å brukes. For å få til dette er det to ting man må ta stilling til.
Den første er hvilken versjon av nginx som brukes. Dette finner man ut med flg. kommando:
nginx -v
Det vesentlige her er hvorvidt denne er høyere eller lavere enn 1.25.1, ettersom konfigurasjonssyntaks endret seg i denne versjonen. Hvis man bruker nginx på Red Hat vil den være lavere, ettersom ingen Red Hat-versjoner foreløpig inneholder nyere versjoner enn dette. Men hvis man f.eks. kjører nginx i en container, eksempelvis på OpenShift, kan det være man har en høyere versjon.
Dersom man har en lavere versjon enn 1.25.1, finner man linjene som starter med listen, og legger til http2 på slutten av disse.
Altså hvis man har en linje som f.eks. ser slik ut:
listen 443 ssl;
Endrer man denne til å se slik ut:
listen 443 ssl http2;
Hvis port 80 er i bruk bør man gjøre tilsvarende endring for tilhørende listen-linje der også, men sørg for å lese neste punkt først.
Hvis man derimot har en høyere versjon enn 1.25.1, legger man istedet til denne linjen:
http2 on;
Denne plasseres da under listen-linjen beskrevet i forrige eksempel.
Det neste man må ta stilling til er om trafikk fra lastbalanserer til backend (altså nginx) går over HTTP (port 80) eller HTTPS (port 443). Dersom den går over HTTPS, vil HTTP/2 bli brukt automatisk (så lenge man har gjort endringen beskrevet over), og man trenger ikke foreta seg noe mer. Hvis man bruker nginx i OpenShift i Passthrough-modus vil HTTPS alltid bli brukt, men man må likevel sørge for å gjøre endringen beskrevet over for at det skal virke.
Hvis den derimot går over HTTP (port 80), vil ikke oppgradering til HTTP/2 skje av seg selv, og lastbalanserer må eksplisitt fortelles at den skal bruke dette. Dette gjør man ved å be www-drift om å legge dette til. Konfigurasjonen man skal be om er dette:
backend_http_version: proto h2
Merk at hvis man bruker nginx i OpenShift i Edge-modus trenger man per i dag ikke å gjøre dette, ettersom det foreløpig ikke støttes i mellomliggende lag (OpenShifts interne router/ingress-punkt).
Komprimering
Komprimering av innhold før det sendes til klient vil forbedre ytelsen på lasting av sider, og dermed brukeropplevelsen. Her er det dog flere hensyn å ta. Ikke alle filtyper er komprimerbare, f.eks. kan man normalt ikke komprimere bilder eller annet mediainnhold. Det finnes dessuten forskjellige komprimeringsalgoritmer å velge mellom, og ikke alle er støttet av alle servere, eller alle klienter. Det riktige valget vil være den beste (og som regel mest moderne) komprimeringsalgoritmen som er støttet av backend, som klient oppgir at den støtter (i request headeren Accept-Encoding).
Når ikke backend komprimerer innhold, vil lastbalanserer automatisk komprimere svaret fra en backend dersom dette ikke alt er komprimert, så fremt filtypen er av en type den vet kan komprimeres. Dette sørger for at komprimering alltid tas i bruk når dette gir mening, for all trafikk som går gjennom lastbalanserer. Likevel er det ønskelig at backend komprimerer innhold der dette er mulig, av flere årsaker:
- Når komprimeringen gjøres av backend istedenfor lastbalanserer, sørger man for at lasten av å komprimere fordeles utover alle tjenester, istedenfor at lastbalanserer må påta seg hele byrden for alle
- Lastbalanserer støtter per i dag ikke de mest moderne komprimeringsalgoritmene, som gjerne er støttet i webservere, og vi har ikke ennå funnet noen måte å integrere dette på
- Lastbalanserer har en øvre antallsbegrensning i listen over filtyper man kan angi at ønskes komprimert
For nginx har vi dessverre foreløpig ikke en ferdig konfigurasjon som kan benyttes, slik vi har for Apache. Dersom noen får til å lage dette, tar vi veldig gjerne imot bidrag! Ta gjerne en titt på konfigurasjonen www-drift benytter på sine Apache-servere for inspirasjon - her vil man kunne se f.eks. hvilke MIME-typer vi dekker. Særlig interessant ville det vært å finne en god måte man kan ta i bruk nginx-modulene for både brotli og zstd, både i RHEL og i containere.
Dersom du ikke har tilgang til filen, ta kontakt med www-drift, så kan vi enten gi deg tilgang eller sende deg en kopi.
Reversproxy
Dersom du skal bruke nginx som reversproxy foran en applikasjonsserver, som beskrevet i generelt-seksjonen, er det noen linjer til som kan være greie å få med, for å sørge for at korrekt klient-IP blir videreformidlet til applikasjonsserveren på riktig måte. I tillegg er det gjerne lurt å angi at man ønsker å benytte HTTP/1.1 (som i skrivende stund er den høyeste HTTP-versjonen nginx støtter som reversproxy). Hvis ikke vil den benytte HTTP/1.0, som ikke kan formidle riktig Host header, og heller ikke støtte keepalive, som gir bedre utnyttelse av forbindelser.
En helhetlig config for reversproxy mot en applikasjonsserver kjørende på localhost på port 8080 kan dermed f.eks. se omtrent slik ut (merk at her må navnet foo.uio.no samt sti til sertifikat byttes ut):
server {
listen 443 http2;
server_name foo.uio.no;
# Specify certificate
ssl_certificate /path/to/foo.uio.no_fullchain.crt;
ssl_certificate_key /path/to/foo.uio.no.key;
# Trusted proxies
real_ip_header X-Forwarded-For;
real_ip_recursive on;
set_real_ip_from w3prod-lb-uio01.uio.no;
set_real_ip_from w3prod-lb-uio02.uio.no;
# Proxy to port 8080 on localhost
location / {
proxy_pass http://localhost:8080/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $http_x_forwarded_for;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}Enkelte guider på nett vil kanskje foreslå at du legger til headere som X-Forwarded-Proto. Dette er ikke nødvendig bak UiOs lastbalansering, som forklart i avsnittet om videresending til HTTPS.
IIS
Også Microsoft Internet Information Services (IIS) har støtte for X-Forwarded-For, selv om denne er noe mer begrenset enn i Apache og nginx. I Apache og nginx vil relevante interne variabler i webserver endre seg slik at klientens IP (hentet fra X-Forwarded-For) i prinsippet erstatter lastbalansererens IP, men i IIS er ikke dette tilfelle. Man vil likevel kunne få riktig IP i logger, samt ha mulighet til å benytte benytte ACLer der dette er relevant.
Vi har dessverre foreløpig ikke dokumentasjon for hvordan dette settes opp i IIS, men vi vet at seksjon for datainnsamling og automasjon (DIA) jobber med å få på plass en felles løsning som skal gjøre det enkelt og automatisk å få IIS til å generere logger i ønsket format for DataOps, med korrekt klient-IP. Inntil videre kan man ta kontakt med DIA for å få hjelp til å sette dette opp riktig.
Applikasjonsservere
Les aller først avsnittet Generelt, som inneholder viktig generell informasjon om hvordan applikasjonsservere bør settes opp. Med applikasjonsserver mener vi i hovedsak egne dedikerte prosesser som server en webapplikasjon, som regel over HTTP. Det finnes mange gode artikler som nok definerer dette bedre enn vi klarer.
Ofte betjenes en tjeneste av en applikasjonsserver. Disse finnes det utallige varianter av, med varierende støtte for slike ting som en webserver kan tilby, som beskrevet i Generelt-avsnittet. Vi anbefaler derfor alltid at en webserver plasseres foran en evt. applikasjonsserver. Følger man instruksjonene på denne siden, vil man få riktig IP i webserver, både i logger og øvrig miljø. I mange tilfeller vil dette være tilstrekkelig.
Når en applikasjonsserver er i bruk, bak en webserver, kan det dog hende man også her ønsker å få tilgang til faktisk klient-IP. Dette kan f.eks. være tilfelle dersom man trenger å generere logger som inneholder brukernavn (sammen med (korrekt) IP), eller andre detaljer som en webserver ikke vil vite om når den kun reversproxyer forespørsler videre. Det kan også være at brukers IP skal benyttes i selve tjenesten på noe vis. Hvordan dette kan løses vil avhenge av hvilken applikasjonsserver man bruker, og hvilke muligheter denne gir.
Applikasjonsservere kan i denne sammenhengen litt forenklet deles opp i to kategorier:
Direkte integrert og andre protokoller
Dette gjelder f.eks.:
- Moduler som kjører direkte i webserver (eksempelvis mod_php eller mod_perl i Apache)
- Tjenester som integrerer over protokollen AJP (Apache JServ Protocol), brukes gjerne mellom Apache og Java applikasjonsservere som Tomcat, JBoss EAP/WildFly og GlassFish/Payara m.fl.
- Tjenester som integrerer over FastCGI/SCGI (eksempelvis php-fpm)
- Tjenester som integrerer over (u)WSGI/ASGI (brukes av diverse Python-løsninger)
- Tjenester som integrerer over CGI (eldre, tregt og mindre sikkert, ikke særlig mye brukt lengre)
I disse tilfellene vil som regel miljø fra webserver arves direkte, slik at variabler vil følge det som er satt i webserver, inkludert kilde-IP. Har man konfigurert ting riktig i webserver skal med andre ord vanligvis dette fungere "out of the box".
Merk at av disse alternativene, er det egentlig kun FastCGI/SCGI og (u)WSGI/ASGI vi normalt ville anbefalt i dag. De andre er nå i realiteten utdaterte, og bør som regel byttes ut med mer moderne og fremtidsrettede løsninger.
For moduler som kjører direkte i webserver (mod_php, mod_perl m.fl.), samt CGI, vil mye av det som står skrevet om applikasjonsservere dessuten ikke være relevant, ettersom disse ikke kjører som egne prosesser uavhengig av webserver, men istedet spawnes av (eller kjører som del av) webserverens egen prosess. Dermed vil de sånn sett ikke egentlig regnes som applikasjonsservere - i hvert fall ikke med definisjonen vi bruker her.
Integrert over HTTP
Dette gjelder tilfeller der webserver snakker med applikasjonsserver kun over HTTP, og dermed fungerer som en ren reversproxy. Dersom tjenesten i prinsippet kan fungere uten behov for en webserver foran, er det trolig dette dere bruker. Merk at vi anbefaler likevel at man setter en webserver mellom lastbalanserer og applikasjonsserver. Se avsnittene Generelt, samt Gjenopprette korrekt IP, for en forklaring av hvorfor dette er anbefalt.
For at en applikasjonsserver skal se korrekt IP i slike tilfeller, må (også) applikasjonsserver kunne konfigureres til å benytte HTTP-headeren X-Forwarded-For (eller Forwarded) og beslektede headere. I hvilken grad dette er støttet, og hvordan støtten er implementert, vil variere stort mellom forskjellige applikasjonsservere, og det finnes altfor mange av disse til at vi kan dekke alle her. Dersom du setter deg fast, kan du gjerne ta kontakt, så skal vi gjøre vårt beste for å hjelpe.
Av headere som kan være nyttige finner man bl.a. X-Forwarded-By som angir hvilken lastbalanserer X-Forwarded-For ble satt av (kan benyttes til å spesifisere hvilke lastbalanserere man vil stole på), X-Forwarded-Proto som angir hvilken protokoll innkommende forespørsel benyttet (kan f.eks. benyttes til å forhindre redirect loop til HTTPS når applikasjonen tror den mottar forespørsler over HTTP) m.fl.
PROXY protocol
Det finnes også en annen måte for lastbalanserer å videreformidle klientens IP på, kalt PROXY protocol, men til forskjell fra X-Forwarded-For opererer denne i TCP-laget. Dette forutsetter at backend støtter denne protokollen (hvilket både Apache og nginx gjør), samt at støtte for den aktiveres for tjenesten i lastbalanserer. Vi bruker ikke PROXY protocol noe særlig per i dag, men den kan være grei å vite om. Den er også beste løsning der andre protokoller enn HTTP skal lastbalanseres, men dette gjøres kun unntaksvis.
Tilgangskontroll
Noen ganger har man behov for å begrense tilgang basert på IP, enten til spesifikke nett (subnett/CIDR) eller til enkelt(e) IP-adresse(r). Et vanlig bruksområde er å begrense tilgang til klienter på UiO-nett. Vi anbefaler at dette gjøres i webserver, som beskrevet i Generelt-avsnittet. Ved å følge eksemplene over, "gjenopprettes" kilde-IP av webserver, slik ting fungerer på samme måte som når det ikke er et revers-proxy-lag med lastbalansering imellom. Dermed vil man kunne benytte vanlig ACL-funksjonalitet i webserver, akkurat som vanlig.
Når man setter opp IP-basert tilgangskontroll, anbefaler vi at det benyttes en egen feilmeldingsside til dette. Denne kan gjerne forklare at tilgang nektes fordi man ikke kommer fra riktig nett eller IP, slik at bruker får en indikasjon på hva som kan eller må gjøres for å få tilgang til tjenesten (selvsagt uten å avsløre potensielt sensitive detaljer). På denne siden kan det gjerne også linkes til andre sider med mer informasjon om dette, der dette finnes. Se et godt eksempel på hvordan dette kan se ut på siden Brukerinfo har opprettet til dette formålet.
Det er også mulig å legge inn IP-basert tilgangskontroll for en tjeneste direkte i lastbalanserer, men dette fraråder vi. Dette er mer komplisert å vedlikeholde, da alle endringer nødvendigvis må involvere www-drift som drifter lastbalanseringsløsningen, til forskjell fra i webserver hvor man styrer dette helt selv. Det vil dessuten resultere i en mindre brukervennlig og hjelpsom feilmeldingsside, da denne vil være en generisk type som ikke gir noen indikasjon på hvorfor tilgang nektes, og kan forveksles med feilmeldinger man mottar når manglende tilgang skyldes helt andre årsaker. Dette kan igjen medføre unødvendig økt trykk på IT-hjelp, som da kan motta saker fra brukere som ikke skjønner hvorfor de ikke får tilgang.
Caching
For å sørge for at en tjeneste oppnår best mulig ytelse, er det essensielt å utnytte de mulighetene man får vha. caching. Caching sørger for at når en klient laster en tidligere besøkt side, så henter den på nytt kun de ressursene som er nødvendig, dvs. de ressursene som man kan forvente at har endret seg siden sist siden ble lastet. Dette sørger både for at tjenesten oppleves raskere og mer responsiv for brukeren, samtidig som det det sparer betydelig ressursbruk i tjenestelaget.
Måten man kommuniserer om og når en cache (f.eks. klientens innebygde cache) trenger å laste ressurser på nytt er i dag i all hovedsak vha. headeren Cache-Control. Andre headere enn denne har vært i bruk tidligere, men disse vil som regel ignoreres så lenge Cache-Control forekommer, og lastbalanserer sørger for at denne så å si alltid gjør det. Dermed er det ikke nødvendig å sette eller bry seg om andre headere enn denne.
Når en tjeneste sender denne headeren i en respons, vil lastbalanserer i de fleste tilfeller respektere verdiene som kommer fra tjenesten, men den vil i visse tilfeller prøve å hjelpe, og i enkelte tilfeller velge å overstyre. Dersom den ser at en respons ikke burde caches, f.eks. dersom responsen setter en sesjons-cookie eller på andre måter vurderes som sensitiv, vil den sørge for at headeren inneholder verdier som tilsier at responsen ikke skal caches i noe lag. Dersom den derimot ser at alt tyder på at responsen aldri vil endre seg på denne adressen - at ressursen er immutable - vil den sørge for at dette videreformidles slik at ressursen kan oppnå optimal caching. Som oftest gjelder dette statiske ressurser hvor adressen er gjort unik, enten ved et versjonsnummer i dir/filnavn, eller når det inkluderes en hex-basert hash enten i filnavn eller i et gjenkjent query-parameter.
Dersom en tjeneste ikke sender Cache-Control i sin respons, vil lastbalanserer i mange tilfeller legge denne på, med relativt konservative verdier basert på innholdets type. Altså vil dokumenter caches ganske kort (bare noen minutter), mens statiske ressurser vil caches lengre, basert på type innhold.
Samlet sett sørger dette for at de fleste forespørsler som regel vil få en egnet caching policy, men i tilfeller der det hersker noen grad av tvil, og tjenesten selv har oppgitt en Cache-Control header, vil altså som regel denne benyttes. Det er derfor viktig at man har et bevisst forhold til denne headeren, og er sikker på at man benytter hensiktsmessige og godt egnede verdier.
Dette er et komplekst tema, og vi kan ikke dekke alt som inngår i dette her. Utviklere og tjenesteeiere anbefales å sette seg inn i dette, og skaffe seg i hvert fall et grovt overblikk over de grunnleggende prinsippene som ligger til grunn. Artikler som Cache-Control for Civilians anbefales for dette.
Vi vil likevel forsøke å gi noen pekepinner under for enkelte best practices knyttet til caching, og således prøve å ta høyde for noen av de vanligste problemene vi ser oppstå knyttet til caching.
Send Cache-Control header hvis du kan
TL;DR: Tjenesten vet som regel best hva som kan og ikke kan caches, samt hvor lenge og hvordan
Når lastbalanserer mottar responser som mangler Cache-Control header, vil den (som oppsummert over) forsøke å gjøre noen delvis kvalifiserte antakelser basert på innholdets type. Disse vil dog sjeldent være like gode som konkret kunnskap om hver spesifikke respons, og denne kunnskapen er det bare tjenesten selv - og de som lager den - som har. Det anbefales derfor å alltid sende en Cache-Control header med alle responser, som er egnet for hver enkelte respons (se punktene under), når man har muligheten til dette. Eksempelvis kan det være at man ønsker dokument-delen av en side alltid skal være fersk - det vil den ikke nødvendigvis være dersom man ikke angir en Cache-Control header som sier dette.
Skill mellom dynamiske og statiske ressurser
TL;DR: Bare sett no-store, no-cache eller lav max-age for de responsene hvor det faktisk trengs!
Dette er trolig punktet det syndes aller mest mot. Man angir en policy per tjeneste heller enn per ressurs, og så lar denne gjelde for alle responser for tjenesten. Dette er som regel en riktig dårlig idé, ettersom det er all grunn til å forskjellsbehandle disse. La oss se på et par eksempler.
Dokumentdelen av en webside - dvs. de responsene som returnerer HTML - kan regnes som statisk hvis den er lik for alle som besøker den. Dette kan være en ferdig generert webside som serves som fil fra filsystemet, men det kan også være statiske sider på en ellers dynamisk webtjeneste. F.eks. kan en egen loginside gjerne regnes som statisk ettersom den er lik for alle, ikke i seg selv inneholder sensitiv informasjon, og sjeldent endres. En statisk webside kan med fordel caches, men kanskje ikke nødvendigvis så veldig lenge.
En webside (igjen dokumentdelen) vil derimot regnes som dynamisk dersom det motsatte er sant. F.eks. at innholdet på en og samme adresse ser forskjellig ut for hvert besøk, basert på slikt som innholdet i en sesjons-cookie, innholdet på selve siden (som kanskje blir generert på nytt ved hver lasting), eller kanskje basert på hvor man besøker den fra (kanskje den kun er tillatt fra enkelte IP-adresser el.l.). Slike sider ønsker man gjerne at ikke caches overhodet, eller kanskje at de caches for sluttbrukeren men ikke i evt. mellomliggende lag, eller f.eks. at den kan serves direkte fra cache men revalideres i bakgrunnen. Her finnes det mange muligheter - se artikkelen Cache-Control for Civilians nevnt tidligere.
Den siste kategorien er dog den vel så viktige her, nemlig støtteressurser. En webside består av en rekke ressurser utover dokumentet, slik som stylesheets, JavaScript, bilder, og kanskje også annet som fonter, video eller audio, og masse annet spennende. Disse ressursene er ofte de tyngste (største) å overføre, og disse ressursene kan som regel caches med stor fordel - men bare hvis man gir lov til det!
Selv på en sensitiv webside er det ofte hensiktsmessig å tillate caching av støtteressursene, nettopp fordi det er ikke disse som er sensitive. Et JavaScript eller en CSS-fil vil gjerne være lik for alle brukere, uavhengig av om brukeren er innlogget eller ikke. Ved å tillate caching av disse, unngår man at disse stadig (og helt unødvendig) må lastes på nytt etter hvert som brukeren navigerer rundt i tjenesten. I de fleste tilfeller gjelder dette også f.eks. bilder og andre ressurser, og da burde også disse få samme liberale policy. I de tilfeller hvor det derimot er nødvendig at f.eks. enkelte bilder kun vises til innloggede brukere, kan disse likevel caches i klientlaget (men ikke i evt. mellomlag), eller man kan f.eks. benytte "Vary: cookie" header (se avsnitt under) for å skille mellom disse.
Oppsummert kan man altså si at man bør definere en caching policy per ressurs og ikke per tjeneste. Gjort riktig vil dette kunne gi optimalt resultat.
Ikke sett for kort max-age
TL;DR: Ikke sett en lav max-age på ressurser som sjeldent endres!
Hvor lenge en respons kan brukes fra cache bestemmes av Cache-Control-direktivet max-age (samt s-maxage for mellomliggende caching-lag dersom denne også er oppgitt). Ofte ser man at denne verdien er satt uforholdsmessig lav, og årsaken til dette er som regel - som forklart i forrige punkt - at det er satt én caching policy basert på dokumenter, som så også blir brukt for støtteressurser, heller enn å skille mellom disse.
For dokumenter gir det gjerne mening å sette en relativt konservativ verdi - i hvert fall for sider som ikke endrer seg så veldig ofte. Men for andre ressurser mister man mye verdi av caching dersom verdien settes for lavt, ettersom ressursen må hentes (eller i det minste sjekkes) på nytt når utløpsalderen er nådd. Hvis man sjeldent oppdaterer en fil, og det ikke er så farlig om enkelte klienter fortsetter å se den gamle filen en stund etter at den er oppdatert, vurder om verdien kanskje kan settes høyere.
Bruk cache busting og immutable
TL;DR: Ta i bruk cache busting der du kan, og sett da immutable i Cache-Control!
Den aller mest effektive måten å tilnærme seg caching på er ved å benytte cache busting. Dette vil si at man sørger for å gi hver unike kopi av en statisk fil en unik URL, ettersom ressurser caches basert på URL. Når en ny versjon av siden så genereres på nytt, f.eks. når man gjør en release, vil alle filer som har endret seg siden forrige gang få nytt unikt navn, og dermed ny URL. Den nye versjonen av siden vil så referere til ressursene sine med det til enhver tid gjeldende navnet, slik at disse lastes enten fra cache, eller fra tjenesten, ut fra hvorvidt den refererte URLen eksisterer i cache. Dermed "buster" man de utdaterte filene fra cache, ved at disse simpelthen ikke lenger refereres.
Det finnes flere måter å gjøre dette på, men den mest anbefalte er å generere en hex hash av filens innhold, og så bruke denne som fingerprint som utgjør del av filnavnet, heller enn i f.eks. en query string eller andre varianter. Se detaljer i Cache-Control for Civilians for en oppsummering av dette.
Når dette er på plass, kan og bør man sette direktivet immutable i Cache-Control-header, samt en veldig lang max-age for å ta høyde for evt. klienter som ikke ennå støtter immutable-direktivet. Da sørger man for at den unike URLen som refererer til den unike kopien av filen aldri vil trenge å lastes på nytt, og har således oppnådd optimal caching.
Mange moderne frontend-rammeverk (f.eks. NextJS) tar seg av alt dette automatisk, og gjør hele jobben for deg. Har du mulighet til å gjøre dette i rammeverket du bruker, er det varmt anbefalt å gjøre dette, samt verifisere at det fungerer som beskrevet.
Vær nøysom ved bruk av Vary: cookie
TL;DR: Ikke sett Vary: cookie på responser hvor dette ikke faktisk trengs!
En header som ikke er Cache-Control men som likevel kan ha stor effekt på caching er Vary. Denne sier hvilke headere i en forespørsel som kan resultere i et annet svar fra tjenesten. Verdier man ofte ser i Vary-headeren er typisk Accept-Encoding, som sier at forskjellige svar vil gis basert på hvilken type komprimering (eller evt. ingen komprimering) klienten oppgir at den støtter. Et svar som er komprimert med f.eks. gzip vil naturlig nok være ulikt fra et ukomprimert svar - selv om innholdet kanskje ser likt ut etter at klienten har dekomprimert det. Derfor er det nødvendig å skille mellom svar på denne måten, og en mellomliggende cache som skal kunne cache responser som ellers er cachebare må da cache en kopi for hver utgave av en side som blir levert - i dette eksempelet en kopi som er gzipet og en kopi som er ukomprimert. Ettersom det er få komprimeringsvarianter i bruk, skalerer dette greit. Cachen holder da altså styr på én kopi per forskjellig verdi av headerne som forekommer i Vary-headeren.
Man kan dog angi et hvilket som helst header-navn som verdi i Vary-headeren, og da blir ting fort vrient, fordi det er vanskelig for mellomliggende cache å vite hvor mange variasjoner det kan finnes som svar på hver av header-verdiene. Dette gjelder særlig når man putter verdien cookie i Vary-headeren, ettersom det i prinsippet vil finnes (minst) like mange forskjellige verdier av cookie som det finnes klienter, og dette kan dermed umulig skalere. I praksis gjør dette det umulig å benytte mellomliggende cache for slike responser, og den vil heller ikke prøve. Dermed mister man et slikt caching-lag.
Så hvorfor skulle man da ønske å gjøre dette? Svaret er ofte for å beskytte ressurser. Dersom f.eks. et bilde kun skal være tilgjengelig for innloggede brukere, eller kun for en viss kategori av innloggede brukere el.l., kan Vary: cookie være en løsning på dette - ettersom det da er den enkelte brukers tilgangskontroll (som bestemmes ut fra brukers sesjon, som igjen bestemmes ut fra brukers sesjons-cookie) som vil avgjøre dette. Med andre ord vil tilgang måtte avgjøres på individnivå, og dette oppnår man ved å gjøre ting på denne måten.
Går man for denne løsningen, er det viktig å sørge for at Vary: cookie bare sendes på de ressursene som er ment å være tilgangskontrollert. Altså vil det være lite hensiktsmessig å sende denne for alle bilder "bare for sikkerhets skyld", ettersom det vil medføre at ingen bilder vil kunne caches i mellomliggende lag - uavhengig av om de egentlig trenger slik beskyttelse. Istedet burde bruken av Vary: cookie i slike tilfeller trigges av at det settes tilgangskontroll på bildet, og med andre ord håndteres per enkeltressurs. Da vil man oppnå både optimal kontroll og optimal caching.
Et annet alternativ kan være å skille mellom max-age og s-maxage i Cache-Control-headeren. Sistnevnte påvirker kun caching i mellomlag, slik at hvis denne der settes til 0, vil man sørge for at ressursene kan caches i klientlaget, men ikke i mellomlag. Man kan også sette direktivet private, som er en direkte beskjed om at ressursen helt enkelt ikke skal lagres i mellomlag. Det er fullt mulig (og veldig vanlig) å kombinere flere direktiv i Cache-Control-headeren, og selv om dette kan virke litt som smør på flesk, er hensikten ofte å sørge for bredest mulig nedslagsfelt, slik at man effektivt kommuniserer samme hensikt til implementasjoner med varierende grad av støtte for enkelte caching-direktiver.
Vær bevisst ved bruk av must-revalidate
TL;DR: Bruk gjerne must-revalidate, men ikke la det påvirke valg av verdi i max-age!
must-revalidate er i utgangspunktet et veldig fint direktiv å bruke, ettersom det gir et fint kompromiss; ressursen serves fra cache, men samtidig revalideres den i bakgrunnen (etter at max-age er nådd), ved å spørre tjenesten om den har endret seg siden siste cachede kopi. Dersom den har det vil den lastes på nytt, men inntil da (eller hvis den ikke har endret seg) vil den cachede versjonen benyttes. Dette direktivet kan med andre ord med fordel brukes, men det må brukes sammen med en max-age, og det er da viktig å sette max-age hensiktsmessig. Det kan være lett å tenke at siden man har satt must-revalidate, kan man sette max-age lavt, ettersom cachen da vil validere responsen på nytt mot server uansett. Noen setter sågar max-age til 0, og tenker da at man slik garanterer en fersk respons fra server hver gang.
Dette stemmer for så vidt i utgangspunktet, men med noen viktige nyanser. For det første vil man da ende med å generere en forespørsel til tjenesten hver gang selv om ressursen er cachet (eller i hvert fall så lenge ikke max-age har utløpt, som jo er umiddelbart dersom den er satt til 0). Om ressursen ikke har endret seg sparer man seg riktig nok fra å måtte sende dataene på nytt, men tjenesten er likevel nødt til å sjekke hver gang.
Videre kan dette påvirke caching i mellomlaget, som kanskje ikke støtter dette direktivet slik klient-cachen gjerne gjør. Ettersom den da kun vil se på max-age, vil den dersom denne er satt til 0 ende med å ikke cache ressursen overhodet, og man mister dermed et helt lag med caching.
must-revalidate er derfor et fint supplement, men som ikke burde ha noen innvirkning på hvilken verdi man setter i max-age. Sett max-age slik du uansett ville satt den, helt uavhengig av must-revalidate.
Bare bruk no-transform når det faktisk er nødvendig
TL;DR: Ikke bruk no-transform med mindre du har et konkret behov som tilsier dette!
no-transform er et direktiv som sier at mellomliggende lag - caching så vel som andre - ikke skal på noe vis endre på payload i responsen som kommer fra tjenesten. Dette kan i utgangspunktet høres tilforlatelig nok ut, men det kan fort ha utilsiktede konsekvenser.
Det mest vesentlige av dette er gjerne komprimering. Å komprimere svaret regnes som å endre på det (å "transformere" det), og lb vil dermed ikke komprimere et tidligere ukomprimert svar, selv når dette ellers ville blitt gjort og hadde gitt gevinst. Dette påvirker dermed ikke nødvendigvis caching i seg selv, men vil likevel ha en negativ effekt på ytelse dersom ikke tjenesten selv sørger for å komprimere svaret (og gjør minst en like god jobb som lastbalanserer ville gjort når den må påta seg dette ansvaret).