Self-hosting
Il Playground è fatto di file statici, e
slides --server è un solo processo sul loopback. Entrambi
possono girare sulla tua macchina, con il tuo nome, per persone che accedono
con l’account della tua organizzazione. Questa pagina lo fa con
Caddy (provato con la v2.11.7), che ottiene
e rinnova da sé i certificati HTTPS, e con
oauth2-proxy per
l’accesso. Ogni file mostrato qui si trova in
selfhost/,
e la sua CI controlla ogni Caddyfile con caddy validate.
Il Playground
Ogni release del sito allega l’intero sito, Playground compreso, come un unico tarball costruito per la radice di un dominio, con il relativo SHA-256:
curl -fLO https://github.com/go-slides/go-slides.github.io/releases/download/v0.1.0/go-slides-site-v0.1.0.tar.gz
curl -fLO https://github.com/go-slides/go-slides.github.io/releases/download/v0.1.0/go-slides-site-v0.1.0.tar.gz.sha256
sha256sum -c go-slides-site-v0.1.0.tar.gz.sha256
tar -xzf go-slides-site-v0.1.0.tar.gz -C /srv
mv /srv/go-slides-site-v0.1.0 /srv/go-slides-siteIl tarball è riproducibile: ricostruire lo stesso tag dà gli stessi
byte. Accanto a ogni file comprimibile (i moduli WebAssembly, i
font, le pagine, gli script) contiene lo stesso file compresso con
Brotli (.br) e gzip (.gz), così il server li invia così come sono e
non comprime nulla al volo.
Caddyfile
slides.example.org è il tuo nome. I suoi record A e AAAA puntano alla
macchina, e le porte 80 e 443 raggiungono Caddy:
import headers.caddy
slides.example.org {
root * /srv/go-slides-site
file_server {
precompressed br gzip
}
import playground_headers
}caddy run --config Caddyfile.playgroundIl Playground si trova allora in https://slides.example.org/playground/.
Cosa deve fare bene il server
- HTTPS. Caddy ottiene il certificato da Let’s Encrypt con ACME
(RFC 8555), dimostrando di possedere
il nome rispondendo sulla porta 80 (HTTP-01) o 443 (TLS-ALPN-01), e lo
rinnova. Una macchina che la CA non può raggiungere può usare invece la sfida
DNS-01 (un record TXT, che richiede un Caddy compilato con il modulo del tuo
provider DNS), oppure un certificato proprio con
tls cert.pem key.pem. Su una macchina di prova,tls internalusa la CA locale di Caddy. application/wasm. Caddy invia questo tipo per.wasm; un altro server potrebbe doverne essere informato. Il Playground compila i propri moduli dai byte che ha letto (il motore di esportazione mentre scarica, in una risposta a sé con quel tipo), quindi parte anche da un server che invia un altro tipo; connosniff, il tipo giusto resta comunque quello da inviare.- Compressione.
precompressed br gzipinviaplayground.wasm.bra un browser che accetta Brotli,.gza uno che accetta gzip, e il file stesso negli altri casi, conVary: Accept-Encoding. Il modulo dell’editor è di circa 7 MiB in Brotli, contro 33 MiB non compresso. - Cache. La pagina richiede ogni modulo, worker, font e immagine
con la release nella query (
playground.wasm?v=v0.1.0), quindi questi possono essere conservati per un anno (immutable). Le pagine stesse vengono rivalidate (no-cache): una nuova release si vede alla visita successiva. - Gli header in
headers.caddy:
(playground_headers) {
header {
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval'; worker-src 'self'; connect-src 'self' https:; img-src 'self' data: blob: https:; media-src 'self' data: blob: https:; style-src 'self' 'unsafe-inline' https:; font-src 'self' data: blob: https:; frame-src 'self'; object-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'"
X-Content-Type-Options nosniff
Referrer-Policy no-referrer
Strict-Transport-Security "max-age=31536000"
}
@versioned query v=*
header @versioned Cache-Control "public, max-age=31536000, immutable"
@unversioned not query v=*
header @unversioned Cache-Control "no-cache"
}La policy è quella di cui il Playground ha bisogno: i suoi script (il loader è
in linea), il suo WebAssembly ('wasm-unsafe-eval') e il suo worker, tutti dal
tuo server. Immagini, fogli di stile e font di una presentazione possono provenire da qualsiasi
indirizzo https:, come nell’output del comando; togli https: da
img-src, style-src, font-src, media-src e connect-src per mantenere
ogni richiesta sul tuo server, e le presentazioni che puntano altrove vengono mostrate senza
quelle risorse. La presentazione stessa gira in iframe in sandbox senza la tua
origine, sotto una policy più rigida di loro proprietà che il Playground scrive
in ogni frame: non può raggiungere il Playground, la sua memoria né i tuoi
cookie, qualunque cosa dica questo header. frame-ancestors 'self' impedisce ad altri
siti di incorporare il Playground in un frame.
- Né COOP né COEP. Il Playground non usa né
SharedArrayBufferné thread, quindi non ha bisogno di essere isolato cross-origin. InviareCross-Origin-Embedder-Policy: require-corpbloccherebbe le immagini di una presentazione provenienti da altri siti.
Senza rete
Una volta caricata la pagina, tutto avviene nel browser: l’editor,
l’anteprima, la vista del relatore, e le esportazioni PDF, PPTX e ODP, i cui
font sono nel tarball. La presentazione non è inviata né al tuo server né
altrove. Ciò che richiede la rete è ciò che la presentazione nomina: immagini o
fogli di stile a un indirizzo https:, e il web font del tema gaia, che
chiede a fonts.bunny.net (senza di esso, le slide ripiegano su un altro
font). I link a questa documentazione escono dal tuo server.
slides --server dietro il proxy
slides --server resta su 127.0.0.1 e Caddy inoltra verso di esso. Il server
controlla l’header Host di ogni richiesta e l’Origin del WebSocket di ogni
ricaricamento in tempo reale, contro il DNS rebinding e le pagine di altri siti, e il
proxy mantiene in entrambi il nome pubblico. --allowed-host aggiunge quel nome a
ciò che i controlli accettano; i controlli restano attivi:
slides -s --allowed-host decks.example.org /srv/decksdecks.example.org {
reverse_proxy 127.0.0.1:8080
}Il ricaricamento in tempo reale passa così com’è (Caddy fa da proxy ai WebSocket senza
configurazione). Non usare --host 0.0.0.0 per questo: servirebbe la
cartella all’intera rete, aggirando il proxy e il suo accesso.
--allowed-host è nella v0.14.0 e successive:
go install github.com/go-slides/slides/cmd/slides@v0.17.1. Richiede Go 1.27.3 o successivo.Accesso con OpenID Connect
Caddy interroga oauth2-proxy per ogni richiesta (forward_auth). oauth2-proxy
risponde 202 a un browser che ha effettuato l’accesso, e Caddy allora serve la richiesta,
e 401 altrimenti, che Caddy trasforma in un reindirizzamento all’accesso. L'
accesso è l’authorization code flow di
OpenID Connect Core 1.0:
il browser va dal tuo provider (Keycloak, Authentik, Entra ID, Google,
Okta, Dex…), torna a /oauth2/callback con un codice, oauth2-proxy
scambia il codice con un ID Token, lo verifica e imposta un proprio cookie.
- Registra un client presso il tuo provider, confidenziale, con l'
authorization code flow. I suoi redirect URI, che devono corrispondere esattamente:
https://slides.example.org/oauth2/callbackehttps://decks.example.org/oauth2/callback. - Metti i segreti in file leggibili solo da oauth2-proxy: il segreto del
client, senza a capo finale, e un segreto per il cookie di
esattamente 32 byte casuali (
head -c 32 /dev/urandom). Li scrive il tuo gestore di segreti (credenziali systemd, segreti Docker o Kubernetes, Vault…); nessuno dei due va in un file di configurazione, in una riga di comando o in una variabile d’ambiente, dove log epsli mostrano. - Configura oauth2-proxy (
oauth2-proxy.cfg):
http_address = "127.0.0.1:4180"
reverse_proxy = true
trusted_proxy_ips = ["127.0.0.1/32", "::1/128"]
upstreams = ["static://202"]
set_xauthrequest = true
provider = "oidc"
oidc_issuer_url = "https://id.example.org/realms/example"
client_id = "go-slides"
client_secret_file = "/run/secrets/oauth2-proxy-client-secret"
code_challenge_method = "S256"
redirect_url = "/oauth2/callback"
whitelist_domains = ["slides.example.org", "decks.example.org"]
email_domains = ["example.org"]
cookie_secret_file = "/run/secrets/oauth2-proxy-cookie-secret"
cookie_secure = true
cookie_samesite = "lax"redirect_url senza host riporta l’accesso di ciascun sito al proprio
callback, così ogni sito mantiene il proprio cookie. whitelist_domains è dove un
accesso può tornare: se gli si chiede di andare altrove, resta sul tuo sito.
trusted_proxy_ips crede agli header X-Forwarded-* solo da Caddy.
email_domains dice chi può entrare; oauth2-proxy ha anche opzioni per
gruppi ed elenchi di indirizzi.
- Configura Caddy (
Caddyfile.oidc):
import headers.caddy
(signed_in) {
handle /oauth2/* {
reverse_proxy 127.0.0.1:4180
}
handle {
forward_auth 127.0.0.1:4180 {
uri /oauth2/auth
copy_headers X-Auth-Request-User X-Auth-Request-Email
@unauthorized status 401
handle_response @unauthorized {
redir * /oauth2/sign_in?rd={scheme}://{hostport}{uri} 302
}
}
{block}
}
}
slides.example.org {
import signed_in {
root * /srv/go-slides-site
file_server {
precompressed br gzip
}
import playground_headers
}
}
decks.example.org {
import signed_in {
reverse_proxy 127.0.0.1:8080
}
}slides -s --allowed-host decks.example.org /srv/decks
oauth2-proxy --config /etc/oauth2-proxy/oauth2-proxy.cfg
caddy run --config Caddyfile.oidcoauth2-proxy e slides ascoltano solo sul loopback: l’unico ingresso passa
da Caddy, e quindi dall’accesso. Gli header del Playground vanno
dentro il blocco con accesso, solo sul Playground: la pagina di accesso di oauth2-proxy
è un modulo, che il loro form-action 'none' bloccherebbe.
L’alternativa è caddy-security,
un modulo di Caddy che gestisce l’accesso dentro Caddy, senza un secondo processo.
Richiede un Caddy compilato con il modulo (xcaddy) e un proprio
linguaggio di configurazione.
slides-gitd: le credenziali restano sul server
slides-gitd (v0.1.0) è il backend git di
un Playground self-hosted. Carica la cartella di una presentazione da una forge,
propone le modifiche in un nuovo branch con una pull (merge) request e cerca le
modifiche a monte, mentre le credenziali della forge restano sul server: il
browser ha solo un cookie di sessione HttpOnly. Sta dietro Caddy e oauth2-proxy
(OIDC), ascolta su 127.0.0.1:8091, prende l’identità dell’utente solo dal
proxy e scrive solo in branch chiamati slides/<utente>/<titolo>-<ora> che il
server nomina da sé.
Non ancora disponibile: la modalità server del Playground, che parla con slides-gitd invece di far tenere un token al browser, è la prossima versione. Oggi slides-gitd funziona e si può verificare, ma il Playground non lo usa.
Configurazione (i segreti sono percorsi di file, mai valori; ogni file deve essere leggibile solo dal proprietario):
{
"listen": "127.0.0.1:8091",
"origin": "https://slides.example.org",
"proxy_secret_file": "/etc/slides-gitd/proxy.secret",
"master_key_file": "/etc/slides-gitd/master.key",
"data_dir": "/var/lib/slides-gitd",
"audit_file": "/var/log/slides-gitd/audit.log",
"forges": {
"github": {"kind": "github", "api": "https://api.github.com", "auth": "github-app",
"app_client_id": "Iv1.…", "installation_id": 12345678,
"app_key_file": "/etc/slides-gitd/github-app.pem"},
"gitlab": {"kind": "gitlab", "api": "https://gitlab.example.org/api/v4", "auth": "user-token"}
},
"users": {
"3f2b…-oidc-sub": {"slug": "ada", "repos": ["github:lab/talks", "gitlab:team/slides"]}
}
}Un utente raggiunge solo le voci forge:proprietario/nome elencate per il suo
soggetto OIDC (negato per impostazione predefinita). Create la chiave master
con head -c 32 /dev/urandom > master.key && chmod 600 master.key, poi
eseguite slides-gitd check -c config.json, che carica tutti i file di segreti
ed esce, e slides-gitd serve -c config.json.
Il lato Caddy, con oauth2-proxy avviato con --set-xauthrequest:
slides.example.org {
handle /oauth2/* {
reverse_proxy 127.0.0.1:4180
}
handle /api/git/* {
# never let a client choose its identity, whatever Caddy's version does
request_header -X-Auth-Request-User
request_header -X-Auth-Request-Email
request_header -X-Gitd-Proxy-Secret
forward_auth 127.0.0.1:4180 {
uri /oauth2/auth
copy_headers X-Auth-Request-User X-Auth-Request-Email
}
request_body { max_size 64MB }
reverse_proxy 127.0.0.1:8091 {
header_up X-Gitd-Proxy-Secret {file./etc/caddy/gitd-proxy.secret}
}
}
handle { root * /srv/slides; file_server }
}Le righe request_header -… eliminano ogni header di identità inviato dal client,
che così non può scegliere chi è; il Caddy recente lo fa da sé prima di
copy_headers, le versioni più vecchie no. {file.*} legge il segreto del
proxy da un file, quindi non sta in una variabile d’ambiente.
GitHub usa una App. Createla in Settings ▸ Developer settings ▸ GitHub Apps con i permessi Contents in lettura e scrittura, Pull requests in lettura e scrittura e Metadata in lettura, senza webhook; installatela solo sui repository delle presentazioni. slides-gitd emette un token di installazione per l’unico repository di ogni chiamata, valido un’ora.
GitLab: ogni utente registra un token di accesso personale o di progetto con
lo scope api. Perché api e non write_repository, e che cosa può fare un
token rubato, è spiegato nella pagina del Playground;
preferite un token di accesso di progetto o di gruppo limitato ai repository
delle presentazioni, o un account dedicato.
Verificarlo
curl -sI -H 'Accept-Encoding: br' 'https://slides.example.org/playground/playground.wasm?v=v0.1.0'La risposta riporta content-type: application/wasm, content-encoding: br
e cache-control: public, max-age=31536000, immutable, con la
content-security-policy vista sopra. Con l’accesso davanti, la stessa
richiesta senza cookie risponde 302 verso /oauth2/sign_in.
Riferimenti
- Caddy: HTTPS automatico,
file_server,header,reverse_proxy,forward_auth. - RFC 8555, ACME: HTTP-01 (8.3), DNS-01 (8.4).
- OpenID Connect Core 1.0: l’authorization code flow (3.1), il redirect URI (3.1.2.1), l’ID Token (2).
- oauth2-proxy: configurazione, provider OpenID Connect.
Created on by tannevaled
Modified on by tannevaled