Auto-hospedagem
O Playground são arquivos estáticos, e o
slides --server é um único processo no loopback. Ambos
podem rodar na sua própria máquina, sob o seu próprio nome, para pessoas que
entram com a conta da sua organização. Esta página faz isso com o
Caddy (testado com a v2.11.7), que obtém e
renova sozinho os certificados HTTPS, e com o
oauth2-proxy para o login. Todos
os arquivos mostrados aqui estão em
selfhost/,
e a CI dele verifica cada Caddyfile com caddy validate.
O Playground
Cada release do site anexa o site inteiro, Playground incluído, como um único tarball construído para a raiz de um domínio, com o seu 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-siteO tarball é reproduzível: construir a mesma tag de novo dá os mesmos bytes. Ao
lado de cada arquivo que comprime (os módulos WebAssembly, as fontes, as
páginas, os scripts) ele traz o mesmo arquivo comprimido com Brotli (.br) e
gzip (.gz), de modo que o servidor os envia como estão e não comprime nada na
hora.
Caddyfile
slides.example.org é o seu nome. Os registros A e AAAA dele apontam para a
máquina, e as portas 80 e 443 chegam ao Caddy:
import headers.caddy
slides.example.org {
root * /srv/go-slides-site
file_server {
precompressed br gzip
}
import playground_headers
}caddy run --config Caddyfile.playgroundO Playground fica então em https://slides.example.org/playground/.
O que o servidor precisa acertar
- HTTPS. O Caddy obtém o certificado da Let’s Encrypt com ACME
(RFC 8555), provando que possui o
nome ao responder na porta 80 (HTTP-01) ou 443 (TLS-ALPN-01), e o renova. Uma
máquina que a CA não consegue alcançar pode usar o desafio DNS-01 (um registro
TXT, que exige um Caddy construído com o módulo do seu provedor de DNS), ou o
seu próprio certificado com
tls cert.pem key.pem. Em uma máquina de teste,tls internalusa a CA local do Caddy. application/wasm. O Caddy envia este tipo para.wasm; outro servidor pode precisar ser instruído. O Playground compila os seus módulos a partir dos bytes que leu (o motor de exportação à medida que baixa, em uma resposta própria com esse tipo), então ele inicia mesmo a partir de um servidor que envia outro tipo; comnosniff, o tipo certo continua sendo o que se deve enviar.- Compressão.
precompressed br gzipenviaplayground.wasm.bra um navegador que aceita Brotli,.gza um que aceita gzip, e o próprio arquivo nos demais casos, comVary: Accept-Encoding. O módulo do editor tem cerca de 7 MiB em Brotli, contra 33 MiB sem compressão. - Cache. A página pede cada módulo, worker, fonte e imagem com a release na
query (
playground.wasm?v=v0.1.0), de modo que eles podem ser mantidos por um ano (immutable). As próprias páginas são revalidadas (no-cache): uma nova release é vista na próxima visita. - Os cabeçalhos em
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"
}A política é a de que o Playground precisa: os seus scripts (o loader é em
linha), o seu WebAssembly ('wasm-unsafe-eval') e o seu worker, todos vindos do
seu servidor. As imagens, folhas de estilo e fontes de uma apresentação podem vir
de qualquer endereço https:, como na saída do comando; retire https: de
img-src, style-src, font-src, media-src e connect-src para manter todas
as requisições no seu servidor, e as apresentações que apontam para outro lugar
são exibidas sem esses recursos. A própria apresentação roda em iframes em
sandbox sem a sua origem, sob uma política própria mais estrita que o Playground
escreve em cada frame: ela não alcança o Playground, o armazenamento dele nem os
seus cookies, diga o que disser este cabeçalho. frame-ancestors 'self' impede
que outros sites incorporem o Playground em frames.
- Nem COOP nem COEP. O Playground não usa
SharedArrayBuffernem threads, então não precisa de isolamento cross-origin. EnviarCross-Origin-Embedder-Policy: require-corpbloquearia as imagens de uma apresentação vindas de outros sites.
Sem a rede
Depois que a página é carregada, tudo acontece no navegador: o editor, a
pré-visualização, a visão do apresentador, e as exportações para PDF, PPTX e
ODP, cujas fontes estão no tarball. A apresentação não é enviada nem ao seu
servidor nem a qualquer outro lugar. O que precisa da rede é o que a
apresentação nomeia: imagens ou folhas de estilo em um endereço https:, e a
fonte web do tema gaia, que ela pede ao fonts.bunny.net (sem ela, os slides
recorrem a outra fonte). Os links para esta documentação saem do seu servidor.
slides --server atrás do proxy
O slides --server fica em 127.0.0.1 e o Caddy encaminha para ele. O servidor
verifica o cabeçalho Host de cada requisição e o Origin do WebSocket de cada
recarga ao vivo, contra DNS rebinding e páginas de outros sites, e o proxy
mantém o nome público nos dois. --allowed-host acrescenta esse nome ao que as
verificações aceitam; elas continuam ativas:
slides -s --allowed-host decks.example.org /srv/decksdecks.example.org {
reverse_proxy 127.0.0.1:8080
}A recarga ao vivo passa como está (o Caddy faz proxy de WebSockets sem
configuração). Não use --host 0.0.0.0 para isso: ele serve o diretório para a
rede inteira, contornando o proxy e o login dele.
--allowed-host está na v0.14.0 e posteriores:
go install github.com/go-slides/slides/cmd/slides@v0.18.0. Requer Go 1.27.3
ou posterior.Login com OpenID Connect
O Caddy consulta o oauth2-proxy sobre cada requisição (forward_auth). O
oauth2-proxy responde 202 para um navegador que fez login, o que o Caddy então
serve, e 401 caso contrário, o que o Caddy transforma em um redirecionamento
para o login. O login é o fluxo de código de autorização do
OpenID Connect Core 1.0:
o navegador vai ao seu provedor (Keycloak, Authentik, Entra ID, Google, Okta,
Dex…), volta a /oauth2/callback com um código, o oauth2-proxy troca o código
por um ID Token, o verifica e define o seu próprio cookie.
- Registre um cliente no seu provedor, confidencial, com o fluxo de código
de autorização. As URIs de redirecionamento dele, que devem coincidir
exatamente:
https://slides.example.org/oauth2/callbackehttps://decks.example.org/oauth2/callback. - Coloque os segredos em arquivos legíveis somente pelo oauth2-proxy: o
segredo do cliente, sem quebra de linha no final, e um segredo de cookie de
exatamente 32 bytes aleatórios (
head -c 32 /dev/urandom). O seu repositório de segredos os escreve (credenciais do systemd, segredos do Docker ou do Kubernetes, Vault…); nenhum dos dois vai para um arquivo de configuração, uma linha de comando ou uma variável de ambiente, onde os logs e opsos mostram. - Configure o 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 sem host leva o login de cada site de volta ao seu próprio
callback, de modo que cada site mantém o seu próprio cookie. whitelist_domains
é para onde um login pode retornar: se pedirem para ir a qualquer outro lugar,
ele permanece no seu site. trusted_proxy_ips acredita nos cabeçalhos
X-Forwarded-* somente do Caddy. email_domains diz quem pode entrar; o
oauth2-proxy também tem opções para grupos e listas de endereços.
- Configure o 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.oidcO oauth2-proxy e o slides escutam somente no loopback: o único caminho de
entrada passa pelo Caddy, e portanto pelo login. Os cabeçalhos do Playground
ficam dentro do bloco de login, somente no Playground: a página de login do
oauth2-proxy é um formulário, que o form-action 'none' deles bloquearia.
A alternativa é o caddy-security,
um módulo do Caddy que faz o login dentro do próprio Caddy, sem um segundo
processo. Ele exige um Caddy construído com o módulo (xcaddy) e tem a sua
própria linguagem de configuração.
slides-gitd: as credenciais ficam no servidor
O slides-gitd (v0.1.0) é o backend de git
de um Playground auto-hospedado. Ele carrega a pasta de uma apresentação de uma
forja, propõe as mudanças em um novo branch com um pull (merge) request e
procura mudanças a montante, enquanto as credenciais da forja ficam no
servidor: o navegador só tem um cookie de sessão HttpOnly. Fica atrás do Caddy
e do oauth2-proxy (OIDC), escuta em 127.0.0.1:8091, toma a identidade do
usuário só do proxy e só escreve em branches chamados
slides/<usuário>/<título>-<hora> que o próprio servidor nomeia.
O modo servidor do Playground, que fala com o slides-gitd em vez de o navegador guardar um token, é descrito em O Playground atrás do slides-gitd.
Configuração (os segredos são caminhos de arquivo, nunca valores; cada arquivo só deve ser legível pelo dono):
{
"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"]}
}
}Um usuário só alcança as entradas forja:dono/nome listadas para o seu sujeito
OIDC (negado por padrão). Crie a chave mestra com
head -c 32 /dev/urandom > master.key && chmod 600 master.key, depois execute
slides-gitd check -c config.json, que carrega todos os arquivos de segredos e
sai, e slides-gitd serve -c config.json.
O lado do Caddy, com o oauth2-proxy iniciado com --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 }
}As linhas request_header -… apagam qualquer cabeçalho de identidade enviado pelo
cliente, que assim não pode escolher quem é; o Caddy recente faz isso sozinho
antes de copy_headers, as versões antigas não. {file.*} lê o segredo do
proxy de um arquivo, então ele não fica em uma variável de ambiente.
O GitHub usa um App. Crie-o em Settings ▸ Developer settings ▸ GitHub Apps com as permissões Contents de leitura e escrita, Pull requests de leitura e escrita e Metadata de leitura, sem webhook; instale-o só nos repositórios das apresentações. O slides-gitd emite um token de instalação para o único repositório de cada chamada, válido por uma hora.
GitLab: cada usuário guarda um token de acesso pessoal ou de projeto com o
escopo api. Por que api e não write_repository, e o que um token roubado
pode fazer, está explicado na página do Playground;
prefira um token de acesso de projeto ou de grupo limitado aos repositórios das
apresentações, ou uma conta dedicada.
O Playground atrás do slides-gitd (modo servidor)
Quando o Playground é servido atrás do slides-gitd em sua própria origem, ele
detecta isso perguntando GET /api/git/session nessa origem. 401 significa
que há um servidor e ninguém fez login: o painel mostra Entrar. Uma sessão
JSON significa que o login foi feito. Qualquer outra coisa, como o 404 de um
host estático, significa que não há servidor, e o painel funciona exatamente
como em go-slides.github.io: um token guardado na aba, ou o login do GitLab.
No modo servidor, o painel esconde os campos de token e de OAuth. Ele mostra
Signed in as <identity> (logado como) e a lista de repositórios que o servidor
concede. Carregar, Propor mudanças… (o servidor nomeia o branch
slides/<readable>-<hash>/…; a página não pode escolher um),
Procurar mudanças e Ficar com as deles vão todos para /api/git/* na
mesma origem. O navegador não contata nenhum host da forja.
O que fica onde:
- Os tokens vivem no servidor, em um cofre AES-GCM com uma chave por usuário.
- O navegador guarda apenas o cookie
__Host-gitd(HttpOnly,Secure,SameSite=Strict), que os scripts não conseguem ler. - O token CSRF fica só na memória da página e nunca é armazenado.
- Toda escrita leva o
Originexato,X-Requested-With: slides-gitd, um corpo JSON eX-CSRF-Token.
Quando o login expirou ou não existe (HTTP 401), o painel mostra “Seu login
terminou” com um botão Entrar (/oauth2/sign_in?rd=<this page> do
oauth2-proxy). Ele nunca recorre a um token digitado no navegador.
Para servi-lo, use selfhost/Caddyfile.gitd de go-slides/go-slides.github.io.
A sua Content-Security-Policy é connect-src 'self'; o site continua público e
só /api/git/* fica atrás do oauth2-proxy. A configuração origin do
slides-gitd deve ser igual à origem do Playground. Uma forja cujo auth é
user-token (GitLab, por exemplo) precisa do token próprio de cada usuário,
guardado no servidor por POST /api/git/credential. O painel não oferece um
campo para isso, então o administrador ou o usuário o faz fora da página.
Um limite: o Playground guarda os rascunhos e o último nome de repositório no perfil do navegador (IndexedDB e localStorage), como antes. Em um perfil de navegador compartilhado, use Draft ▸ Forget my drafts ou perfis separados. No modo servidor, um repositório lembrado ao qual o usuário não tem acesso é descartado.
Verificar
curl -sI -H 'Accept-Encoding: br' 'https://slides.example.org/playground/playground.wasm?v=v0.1.0'A resposta diz content-type: application/wasm, content-encoding: br e
cache-control: public, max-age=31536000, immutable, com o
content-security-policy acima. Com o login na frente, a mesma requisição sem
cookie responde 302 para /oauth2/sign_in.
Referências
- Caddy: HTTPS automático,
file_server,header,reverse_proxy,forward_auth. - RFC 8555, ACME: HTTP-01 (8.3), DNS-01 (8.4).
- OpenID Connect Core 1.0: o fluxo de código de autorização (3.1), a URI de redirecionamento (3.1.2.1), o ID Token (2).
- oauth2-proxy: configuração, provedor OpenID Connect.
Created on by tannevaled
Modified on by tannevaled