Self-hosting
Playground to pliki statyczne, a
slides --server to jeden proces na pętli zwrotnej. Oba
mogą działać na twojej maszynie, pod twoją nazwą, dla osób, które logują się
kontem twojej organizacji. Ta strona robi to za pomocą
Caddy (testowane z v2.11.7), który sam uzyskuje
i odnawia certyfikaty HTTPS, oraz
oauth2-proxy do
logowania. Każdy pokazany tu plik jest w
selfhost/,
a jego CI sprawdza każdy Caddyfile poleceniem caddy validate.
Playground
Każde wydanie witryny dołącza całą witrynę, wraz z Playground, jako jeden tarball zbudowany dla korzenia domeny, z jego 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-siteTarball jest odtwarzalny: ponowne zbudowanie tego samego tagu daje te same
bajty. Obok każdego pliku, który się kompresuje (moduły WebAssembly,
czcionki, strony, skrypty), zawiera ten sam plik skompresowany
Brotli (.br) i gzip (.gz), więc serwer wysyła je tak, jak są, i
niczego nie kompresuje w locie.
Caddyfile
slides.example.org to twoja nazwa. Jej rekordy A i AAAA wskazują
maszynę, a porty 80 i 443 docierają do Caddy:
import headers.caddy
slides.example.org {
root * /srv/go-slides-site
file_server {
precompressed br gzip
}
import playground_headers
}caddy run --config Caddyfile.playgroundPlayground jest wtedy pod adresem https://slides.example.org/playground/.
Co serwer musi zrobić dobrze
- HTTPS. Caddy dostaje certyfikat od Let’s Encrypt przez ACME
(RFC 8555), dowodząc, że posiada
nazwę, odpowiadając na porcie 80 (HTTP-01) lub 443 (TLS-ALPN-01), i
go odnawia. Maszyna, do której CA nie ma dostępu, może użyć zamiast tego
wyzwania DNS-01 (rekord TXT, wymagający Caddy zbudowanego z modułem twojego
dostawcy DNS) albo własnego certyfikatu z
tls cert.pem key.pem. Na maszynie testowejtls internalużywa lokalnego CA Caddy. application/wasm. Caddy wysyła ten typ dla.wasm; inny serwer może wymagać, by mu to powiedzieć. Playground kompiluje swoje moduły z odczytanych bajtów (silnik eksportu w trakcie pobierania, w osobnej odpowiedzi z tym typem), więc startuje nawet z serwera wysyłającego inny typ; przynosniffwłaściwy typ nadal jest tym, który należy wysłać.- Kompresja.
precompressed br gzipwysyłaplayground.wasm.brdo przeglądarki akceptującej Brotli,.gzdo akceptującej gzip, a w pozostałych przypadkach sam plik, zVary: Accept-Encoding. Moduł edytora ma około 7 MiB w Brotli wobec 33 MiB bez kompresji. - Cache. Strona żąda każdego modułu, workera, czcionki i obrazu
z wydaniem w zapytaniu (
playground.wasm?v=v0.1.0), więc można je trzymać przez rok (immutable). Same strony są rewalidowane (no-cache): nowe wydanie jest widoczne przy następnej wizycie. - Nagłówki w
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"
}Polityka jest taka, jakiej potrzebuje Playground: jego skrypty (loader jest
wbudowany), jego WebAssembly ('wasm-unsafe-eval') i jego worker, wszystko z
twojego serwera. Obrazy, arkusze stylów i czcionki prezentacji mogą pochodzić z dowolnego
adresu https:, tak jak w wyniku polecenia; usuń https: z
img-src, style-src, font-src, media-src i connect-src, by zatrzymać
każde żądanie na twoim serwerze, a prezentacje wskazujące gdzie indziej pokażą się bez
tych zasobów. Sama prezentacja działa w iframe z piaskownicą bez twojego
pochodzenia, pod własną, surowszą polityką, którą Playground zapisuje
w każdej ramce: nie sięgnie do Playground, jego pamięci ani twoich
ciasteczek, cokolwiek mówi ten nagłówek. frame-ancestors 'self' powstrzymuje inne
witryny przed osadzaniem Playground w ramce.
- Bez COOP ani COEP. Playground nie używa ani
SharedArrayBuffer, ani wątków, więc nie musi być odizolowany między źródłami. WysłanieCross-Origin-Embedder-Policy: require-corpzablokowałoby obrazy prezentacji z innych witryn.
Bez sieci
Po załadowaniu strony wszystko dzieje się w przeglądarce: edytor,
podgląd, widok prelegenta i eksporty PDF, PPTX i ODP, których
czcionki są w tarballu. Prezentacja nie jest wysyłana ani na twój serwer, ani
nigdzie indziej. Sieci potrzebuje to, co nazywa prezentacja: obrazy lub
arkusze stylów pod adresem https: oraz czcionka internetowa motywu gaia, którą
pobiera z fonts.bunny.net (bez niej slajdy wracają do innej
czcionki). Odnośniki do tej dokumentacji wychodzą z twojego serwera.
slides --server za proxy
slides --server zostaje na 127.0.0.1, a Caddy przekazuje do niego żądania. Serwer
sprawdza nagłówek Host każdego żądania i Origin każdego WebSocketu
przeładowania na żywo, przeciw DNS rebinding i stronom innych witryn, a
proxy zachowuje w obu publiczną nazwę. --allowed-host dodaje tę nazwę do
tego, co kontrole akceptują; pozostają włączone:
slides -s --allowed-host decks.example.org /srv/decksdecks.example.org {
reverse_proxy 127.0.0.1:8080
}Przeładowanie na żywo przechodzi bez zmian (Caddy proxuje WebSockety bez
konfiguracji). Nie używaj do tego --host 0.0.0.0: serwuje
katalog całej sieci, z pominięciem proxy i jego logowania.
--allowed-host jest w v0.14.0 i nowszych:
go install github.com/go-slides/slides/cmd/slides@v0.20.1. Wymaga Go 1.27.3
lub nowszego.Logowanie przez OpenID Connect
Caddy pyta oauth2-proxy o każde żądanie (forward_auth). oauth2-proxy
odpowiada 202 dla przeglądarki, która się zalogowała, co Caddy wtedy serwuje,
a w przeciwnym razie 401, które Caddy zamienia na przekierowanie do logowania.
Logowanie to przepływ kodu autoryzacji
OpenID Connect Core 1.0:
przeglądarka idzie do twojego dostawcy (Keycloak, Authentik, Entra ID, Google,
Okta, Dex…), wraca do /oauth2/callback z kodem, oauth2-proxy
wymienia kod na ID Token, sprawdza go i ustawia własne ciasteczko.
- Zarejestruj klienta u swojego dostawcy, poufnego, z przepływem
kodu autoryzacji. Jego adresy przekierowania, które muszą dokładnie pasować:
https://slides.example.org/oauth2/callbackihttps://decks.example.org/oauth2/callback. - Umieść sekrety w plikach czytelnych tylko dla oauth2-proxy: sekret
klienta, bez znaku nowej linii na końcu, oraz sekret ciasteczka z
dokładnie 32 losowych bajtów (
head -c 32 /dev/urandom). Zapisuje je twój magazyn sekretów (poświadczenia systemd, sekrety Dockera lub Kubernetesa, Vault…); żaden nie trafia do pliku konfiguracyjnego, wiersza poleceń ani zmiennej środowiskowej, gdzie pokazują go logi ips. - Skonfiguruj 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 bez hosta sprowadza logowanie każdej witryny do jej własnego
callbacku, więc każda witryna ma własne ciasteczko. whitelist_domains to miejsca, do
których logowanie może wrócić: poproszone o przejście gdzie indziej, zostaje na twojej witrynie.
trusted_proxy_ips wierzy nagłówkom X-Forwarded-* tylko od Caddy.
email_domains mówi, kto może wejść; oauth2-proxy ma też opcje dla
grup i list adresów.
- Skonfiguruj 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 i slides nasłuchują tylko na pętli zwrotnej: jedyne wejście
prowadzi przez Caddy, a więc przez logowanie. Nagłówki Playground idą
do bloku signed-in, tylko dla samego Playground: strona logowania oauth2-proxy
to formularz, który zatrzymałoby ich form-action 'none'.
Alternatywą jest caddy-security,
moduł Caddy, który realizuje logowanie wewnątrz Caddy, bez drugiego procesu.
Wymaga Caddy zbudowanego z tym modułem (xcaddy) i własnego
języka konfiguracji.
slides-gitd: dane uwierzytelniające zostają na serwerze
slides-gitd (v0.1.0) to backend gita
samodzielnie hostowanego Playgrounda. Wczytuje folder prezentacji z forge,
proponuje zmiany w nowej gałęzi z pull (merge) requestem i sprawdza zmiany w
źródle, a dane uwierzytelniające forge zostają na serwerze: przeglądarka
ma tylko ciasteczko sesji HttpOnly. Stoi za Caddy i oauth2-proxy (OIDC),
nasłuchuje na 127.0.0.1:8091, tożsamość użytkownika bierze tylko od proxy i
zapisuje tylko do gałęzi o nazwie slides/<użytkownik>/<tytuł>-<czas>, które
nazywa sam serwer.
Tryb serwerowy Playgrounda, który rozmawia z slides-gitd zamiast trzymać token w przeglądarce, opisuje sekcja Playground za slides-gitd.
Konfiguracja (sekrety to ścieżki plików, nigdy wartości; każdy plik ma być czytelny tylko dla właściciela):
{
"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"]}
}
}Użytkownik sięga tylko do wpisów forge:właściciel/nazwa wymienionych dla jego
podmiotu OIDC (domyślnie odmowa). Utwórz klucz główny poleceniem
head -c 32 /dev/urandom > master.key && chmod 600 master.key, potem uruchom
slides-gitd check -c config.json, które wczytuje wszystkie pliki sekretów i
kończy, oraz slides-gitd serve -c config.json.
Strona Caddy, z oauth2-proxy uruchomionym z --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 }
}Wiersze request_header -… usuwają każdy nagłówek tożsamości wysłany przez
klienta, więc klient nie może wybrać, kim jest; nowy Caddy robi to sam przed
copy_headers, starsze wersje nie. {file.*} czyta sekret proxy z pliku, więc
nie ma go w zmiennej środowiskowej.
GitHub używa aplikacji (App). Utwórz ją w Settings ▸ Developer settings ▸ GitHub Apps z uprawnieniami Contents odczyt i zapis, Pull requests odczyt i zapis oraz Metadata odczyt, bez webhooka; zainstaluj ją tylko w repozytoriach prezentacji. slides-gitd wystawia token instalacji dla jednego repozytorium każdego wywołania, ważny godzinę.
GitLab: każdy użytkownik zapisuje osobisty token dostępu lub token projektu
z zakresem api. Dlaczego api, a nie write_repository, i co może zrobić
skradziony token, wyjaśnia strona Playgrounda;
wybierz token dostępu projektu lub grupy ograniczony do repozytoriów
prezentacji albo osobne konto.
Playground za slides-gitd (tryb serwerowy)
Gdy Playground jest serwowany za slides-gitd z własnego origin, wykrywa to,
pytając GET /api/git/session na tym origin. 401 znaczy, że serwer jest, ale
nikt nie jest zalogowany: panel pokazuje Zaloguj się. Sesja JSON znaczy, że
użytkownik jest zalogowany. Cokolwiek innego, na przykład 404 hosta
statycznego, znaczy, że serwera nie ma, a panel działa dokładnie jak na
go-slides.github.io: z tokenem trzymanym w karcie albo z logowaniem GitLab.
W trybie serwerowym panel ukrywa pola tokenu i OAuth. Pokazuje
Signed in as <identity> (zalogowano jako) i listę repozytoriów, które serwer
udostępnia. Wczytaj, Zaproponuj zmiany… (serwer nazywa gałąź
slides/<readable>-<hash>/…; strona nie może wybrać własnej),
Sprawdź zmiany i Weź ich wersję trafiają do /api/git/* na tym samym
origin. Przeglądarka nie łączy się z żadnym hostem forge.
Co zostaje gdzie:
- Tokeny leżą na serwerze, w skarbcu AES-GCM z jednym kluczem na użytkownika.
- Przeglądarka trzyma tylko ciasteczko
__Host-gitd(HttpOnly,Secure,SameSite=Strict), którego skrypty nie mogą odczytać. - Token CSRF jest trzymany tylko w pamięci strony i nigdy nie jest zapisywany.
- Każdy zapis niesie dokładny
Origin,X-Requested-With: slides-gitd, treść JSON iX-CSRF-Token.
Gdy logowanie wygasło lub go brak (HTTP 401), panel pokazuje „Twoje logowanie
dobiegło końca” z przyciskiem Zaloguj się
(/oauth2/sign_in?rd=<this page> z oauth2-proxy). Nigdy nie wraca do tokenu
wpisanego w przeglądarce.
Aby go serwować, użyj selfhost/Caddyfile.gitd z go-slides/go-slides.github.io.
Jego Content-Security-Policy to connect-src 'self'; witryna pozostaje
publiczna, a za oauth2-proxy jest tylko /api/git/*. Ustawienie origin
slides-gitd musi być równe origin Playgrounda. Forge, której auth to
user-token (na przykład GitLab), wymaga własnego tokenu każdego użytkownika,
zapisanego na serwerze przez POST /api/git/credential. Panel nie oferuje na to
pola, więc robi to administrator lub użytkownik poza stroną.
Jedno ograniczenie: Playground, jak dotąd, trzyma szkice i ostatnią nazwę repozytorium w profilu przeglądarki (IndexedDB i localStorage). We wspólnym profilu przeglądarki użyj Draft ▸ Forget my drafts albo osobnych profili. W trybie serwerowym zapamiętane repozytorium, do którego użytkownik nie ma dostępu, jest pomijane.
Sprawdź to
curl -sI -H 'Accept-Encoding: br' 'https://slides.example.org/playground/playground.wasm?v=v0.1.0'Odpowiedź mówi content-type: application/wasm, content-encoding: br
i cache-control: public, max-age=31536000, immutable, z powyższym
content-security-policy. Przy logowaniu z przodu to samo
żądanie bez ciasteczka dostaje odpowiedź 302 do /oauth2/sign_in.
Źródła
- Caddy: automatic HTTPS,
file_server,header,reverse_proxy,forward_auth. - RFC 8555, ACME: HTTP-01 (8.3), DNS-01 (8.4).
- OpenID Connect Core 1.0: przepływ kodu autoryzacji (3.1), adres przekierowania (3.1.2.1), ID Token (2).
- oauth2-proxy: configuration, OpenID Connect provider.
Utworzono przez tannevaled
Zmodyfikowano przez tannevaled