Repository navigation
Expand file tree
/
Copy pathCaddyfile.example
More file actions
415 lines (392 loc) · 16.4 KB
/
Copy pathCaddyfile.example
File metadata and controls
415 lines (392 loc) · 16.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
# Janus — production-shaped example config: the guided tour
#
# START FROM Caddyfile.minimal — the stripped-down starting point most
# edges actually need (an app site is just `janus` in a site block).
# This file is the full walkthrough: every capability, every knob, and
# why each site below opts in or out. Take what applies; delete freely.
#
# Operator-facing companion to the root Caddyfile (the working dev config
# on *.janus.localhost). Domains here are example.com placeholders; substitute
# your own. Check it without serving anything:
#
# ./bin/janus validate --config Caddyfile.example --adapter caddyfile
#
# Cold vs hot, in one breath: this file admits CAPABILITIES — what a host
# may do (ping, hub, auth, files) and where /1.0 listens. The hot /1.0 API wires
# TENANTS — which hosts map to which worker sockets, heartbeats, the hub
# bridge, and publish. Nothing here names an app or a socket; a
# tenant registers itself against the control plane:
#
# POST /1.0/apps {"name":"shop","hosts":["www.example.com"],"bridge":"/rt/bridge"}
# PUT /1.0/apps/{id}/upstreams {"upstreams":[{"unix":"run/shop-1.sock"}]}
# POST /1.0/apps/{id}/heartbeat
# POST /1.0/apps/{id}/hub/publish {"@":["/lobby"],"note":{"text":"hi"}}
#
# The registry is memory-only: tenants re-register after a restart.
#
# TLS: sites below carry no tls directive on purpose — Caddy manages
# certificates (ACME) for every named host, which is the production
# default. Local names (*.localhost, *.local) get certificates from
# Caddy's internal CA instead, with no tls directive either.
{
# A stop, restart, or reload gives open connections this long to
# finish, then closes them. Caddy's default is no bound at all: it
# waits for the last connection forever, and a hub socket is open
# forever, so an edge asked to stop would close its listeners and
# never exit. The seeded service Caddyfile carries the same line.
grace_period 10s
# On-demand TLS: before minting a certificate for a name it has no
# cert for, Caddy asks Janus, in-process. The registry allows a host
# a registered app claims and denies everything else — registration
# IS the allowlist; there is nothing separate to manage. (The same
# rule answers GET /1.0/tls/ask for a Caddy that must use `ask`.)
on_demand_tls {
permission janus
}
janus {
# 1) ping — site-scoped, cascades. Global on means every site
# below answers GET /ping with pong unless it says `ping off`:
# unmentioned inherits, explicit off beats inherited on, and the
# built-in default is off when unset everywhere. A tiny
# data-plane liveness probe for smoke tests and load balancers —
# distinct from the control plane's GET /1.0/health.
ping
# 2) control — process-wide /1.0 listeners; never legal in a
# site block (one process, one registry). Each line is
# self-contained: mode, optional listen target, optional token:…
# on the same line. With no control line at all, Janus opens one
# implicit `control internal`.
control internal # unix socket run/janus.sock — operators on the box
control local # http://127.0.0.1:7600/ — tenants, and the tls ask above
# Token forms, per listener: token:NAME (unquoted, no slash)
# reads the secret from that environment variable;
# token:path/with/slash reads it from a file (trailing newline
# stripped); "token:literal" (whole argument quoted) is a
# literal, never legal on public. public requires https:// and
# token:…, and the secret resolves at provision — export or
# create it before starting, or the config refuses to load.
# TLS material, per listener: cert:<path> key:<path>, both
# required on every TLS listener (public, or an https:// listen):
#
# control public token:JANUS_TOKEN cert:/etc/janus/tls.crt key:/etc/janus/tls.key
# control public https://0.0.0.0:7601/ token:/etc/janus/control.token cert:/etc/janus/tls.crt key:/etc/janus/tls.key
# control local http://127.0.0.1:7600/ token:JANUS_TOKEN
# 3) hub — per-app WebSocket fan-out. Site-scoped, cascades like
# ping; every knob here is per-site admission policy
# (no process-wide-only keys). Sockets terminate at Janus and
# survive app reloads; the tenant observes frames through its
# hot-registered bridge and steers via
# POST /1.0/apps/{id}/hub/publish — cold admits the capability,
# hot wires the tenant. Counters: GET /1.0/hub.
hub {
mode bridge # bridge requires bridge/upstreams; direct performs no worker bridge while retaining origin/auth/caps
path /hub # WS endpoint; the hub claims Upgrade requests here only — plain HTTP on the same path flows to the worker
max_conns 4096 # per app, not per site — see the floor caveat at ws-small/ws-large below
max_frame 64kb # largest client frame; oversize closes 1009. Raising it raises the per-connection memory budget
max_channels 128 # channels one connection may join; the whole frame rejects past it
origin same # browsers must present an Origin matching the request Host — closes cross-site WebSocket hijacking by default
}
# 4) mdns — LAN presence. Process-wide, like control; never legal
# in a site block. ENABLING IS AN ANNOUNCEMENT: this machine and
# its app list become visible over multicast DNS, and the
# read-only status front door serves on EVERY interface of this
# machine, not just the LAN — firewall the port (or leave mdns
# off) on a box with a public address. janus.local answers with
# no DNS server or client install; every registered single-label
# .local host advertises the moment its registration lands, and
# withdraws on DELETE or heartbeat reap. With no `listen`, the
# front door rides inside the normal :80 HTTP server behind the
# http://*.local site block below — auto-HTTPS redirects stay
# enabled, and janus decides per request. Advertiser state:
# GET /1.0/mdns.
mdns {
name janus.local # default; <label>.local only
canonical https://janus.lan.example.com # optional: probe + hand-off to real HTTPS; diagnostic page when a rebinding filter eats the name
apps on # default: registered .local hosts advertise too
# listen :7680 # optional: a dedicated front-door listener instead of the shared :80
# interface en0 # optional pinning; default: every multicast-capable interface, live
}
# 5) auth — URL-prefix gates for auth-less apps. Site-scoped,
# cascades like ping and hub: this global block is the process
# default; sites opt out with `auth off` or replace the whole
# config with their own `auth { … }` (users + gates — no merge).
# Bare `auth` / `auth on` inherits. Each gate is a path prefix
# with an allow list; login door is exact `{prefix}auth`. One
# host-wide `__Host-janus` session; sign in/out once. Longest
# prefix wins. Outside every gate is open (strip/421 still apply
# when auth is on). Mint credentials with `janus passhash`
# (argon2id; password prompted, never argv). PERIMETER: guarded
# upstreams must bind localhost or unix sockets. Observe/revoke:
# GET /1.0/auth, DELETE /1.0/auth/sessions[/{id}]. Catch-alls
# need an explicit `auth off` unless you mean to gate them.
auth {
users {
alice adoBxXzvgqIE6iavsfd02vuzcJSC7sVh # janus passhash → paste (version a passhash)
}
ttl 8h # host-wide sliding idle; default 8h
gate / {
# whole site; door /auth
alice
}
}
# 6) files — site-scoped, cascades, built-in off. Hot app
# registration supplies ordered roots, worker-first prefixes, and
# the SPA shell. Patterned registrations additionally supply
# `{site}.suffix`, aliases, and the direct-child directory gate.
# Janus strips client Rip-Site and injects the resolved site label.
# Precompressed transparently serves same-root .br, .zst, or .gz
# sidecars when the canonical file exists and the client accepts one.
files {
precompressed
}
# 7) sendfile — always-on final X-Sendfile transformation;
# no configuration and no toggle (cascades: no).
# 8) browse — site-scoped admission with process-wide theme and
# renderer definitions. The embedded theme is used here. Renderer
# commands are argv, never shell text; exactly one token is
# `{file}`. GET /1.0/browse reports redacted state and counters.
browse {
timeout 10s
max_output 8MB
concurrency 4
renderer .md {
command cat {file}
content_type "text/plain; charset=utf-8"
concurrency 2
}
}
}
}
(janus_access) {
# Capability 9 publishes only access entries that Caddy invokes through
# this encoder. This durable JSON sink remains authoritative.
log {
output stderr
format janus {
time_format rfc3339_nano
duration_format seconds
}
}
}
# The mdns front door: front-door names serve the status page here; every
# other host is redirected to HTTPS by the janus handler itself. auth
# off deliberately keeps this local dashboard public. To protect it,
# inherit the root gate on both this HTTP site and matching HTTPS sites:
# HTTP GET/HEAD then redirects to HTTPS, where /auth provides login.
# Trust onboarding stays public; credentials require HTTPS.
http://*.local {
import janus_access
janus {
auth off
files off
browse off
}
}
# The main site — the empty janus block admits the host and inherits
# every global default: ping on, hub on at /hub, auth on
# with the global users. An unmentioned capability is an inherited one;
# that is the cascade's common case, and most sites should look exactly
# like this.
example.com, www.example.com {
import janus_access
janus
}
# API — explicit off beats inherited on. A plain HTTP API speaks no hub;
# leaving it on would let anyone open sockets against
# a tenant that never bridges. And this API authenticates itself (the
# custom headers above), so the cookie wall opts out too.
api.example.com {
import janus_access
janus {
hub off
auth off
files off
browse off
}
}
# Notes — site auth { … } replaces the global config (carol only; alice
# cannot open this door). A session from example.com does not pass here
# unless carol is in this site's users and the winning gate allow list.
notes.example.com {
import janus_access
janus {
auth {
users {
carol aFaQ0mTJP3Xe7IBvtqP1lshDzU4DJsMZ # version a passhash
}
ttl 30m
gate / {
carol
}
}
}
}
# DuckDB UI — the worked example of what auth is for: an app with no
# login story of its own, reachable only through Janus.
#
# The UI binds loopback inside an incus container and has no
# authentication at all, so anything that reaches its port can run SQL.
# One incus proxy device carries it out of the container's namespace and
# lands it on a unix socket, which is the only kind of upstream Janus
# dials — and is exactly the perimeter the auth capability requires:
#
# incus config device add ct duckui proxy bind=host \
# listen=unix:/opt/janus/run/duckui.sock connect=tcp:[::1]:9497 \
# uid=997 gid=986 mode=0660
#
# Three things in that command are easy to get wrong. The port is
# whatever the launcher set ui_local_port to (9497 here), not the
# extension's 4213 default — check with `ss -ltnp` rather than assuming.
# The UI listens on IPv6 loopback ONLY, so connect=tcp:127.0.0.1:… dials
# nothing; it must be [::1]. And uid/gid/mode must name the janus user,
# because the socket lives in Janus's own runtime dir (drwxr-x--- and
# janus-only) and Janus has to be able to write to it to connect.
#
# Then register it, because the wall runs before host resolution and an
# unregistered host is a 404 whether or not the caller signed in:
#
# curl -sX POST http://127.0.0.1:7600/1.0/apps \
# -H 'Content-Type: application/json' \
# -d '{"name":"duckui","hosts":["duckui.example.com"],
# "lease":"process",
# "upstreams":[{"path":"/opt/janus/run/duckui.sock"}]}'
#
# lease:"process" is load-bearing. The default heartbeat lease is reaped
# by the 15s TTL sweep, and a long-lived app nobody is heartbeating for
# will simply vanish; the process lease is skipped by that sweep. It does
# NOT survive a janus restart or a reboot, and nothing rebuilds it — so
# the launcher should re-register on start rather than treating this as
# one-time setup.
#
# Everything else is off on purpose. hub: the UI speaks its own SSE, not Janus's
# WebSocket. files/browse: nothing static is served from here.
#
# The UI also keeps its own cross-site check, and it has to be told the
# name it now answers to, or every query 401s while the page still
# loads. Set UI_PUBLIC_URL=https://duckui.example.com in the container.
# Do NOT rewrite Origin in a proxy to satisfy it: overwriting the header
# means the check can never fail, which removes the protection rather
# than relocating it. Janus decides who; that check decides from where.
duckui.example.com {
import janus_access
janus {
auth {
users {
dave a7kQxZ3mNvB8pR2sT5wY9cH4jL6nF0gK # janus passhash
}
ttl 2h # shorter than the 8h default: this door opens a database
gate / {
dave
}
}
hub off
files off
browse off
}
}
# Chat — hub tuning override plus an origin allowlist: same-origin
# browsers, and the admin console served from another host. `origin
# same admin.example.com` extends `same` rather than replacing it.
# Non-browser clients send no Origin header and fail `same`; the
# spelled-out opt-out for token-authenticated machine clients is
# `origin any` — a deliberate choice, never a silent default. auth off
# here: those machine clients speak tokens, not cookies (a WS upgrade
# without a session answers 401 on a guarded site).
chat.example.com {
import janus_access
janus {
hub {
max_conns 16384
origin same admin.example.com
}
auth off
}
}
# Direct Hub: edge-only connections and fan-out. Empty upstreams and no
# bridge are valid; control-plane publish remains available.
live.example.com {
import janus_access
janus {
hub {
mode direct
origin same
}
auth off
}
}
# The max_conns floor caveat: the cap is per APP, resolved as the
# MINIMUM effective max_conns across every hub-enabled host the app
# claims. One app claiming both hosts below admits at most 100
# connections total, from either host — adding a tightly capped host
# lowers the whole app's floor. Size the per-connection memory budget
# (~3 MiB adversarial ceiling per connection) against this floor.
ws-small.example.com {
import janus_access
janus {
hub {
max_conns 100
}
auth off
}
}
ws-large.example.com {
import janus_access
janus {
hub {
max_conns 1000
}
auth off
}
}
# Persistent cold browse roots are configuration, not app records. Every
# such handler requires finite exact host matchers; an omitted root policy
# means revalidate. This site has no worker, heartbeat, shell, or DELETE endpoint.
downloads.example.com {
import janus_access
janus {
browse {
root /srv/downloads
root /srv/releases forever
}
}
}
# 11) vista — VistA's RPC Broker over HTTPS. Each POST /rpc carries one
# broker message to the session's own em process (a service manager
# starts one per connection on the Unix socket) and its reply back; a
# TCPConnect starts a session and answers with a __Host-ov cookie, which
# rotates when the user signs on. Not a cascade: routes live on exact
# hosts. The global gate / covers /rpc, so the route states its stance:
# VistA's sign-on authenticates its sessions (`auth exempt`), and every
# other path on this host stays behind the gate. Serve the route on a
# host that carries nothing untrusted: every app on it shares the origin.
vista.example.com {
import janus_access
janus {
# The socket path holds at most 103 bytes (macOS's sun_path).
vista /rpc unix//run/openvista/rpc.sock {
auth exempt
# max_sessions 256 · max_unsigned 32 · per_client 64 · per_client_unsigned 8
# per_client_exempt 10.20.0.0/24 # terminal servers: many users, one address
# signon_timeout 180s · rpc_timeout 10m · queue 8 · max_request 1MiB · max_reply 64MiB
}
}
}
# On-demand TLS — the catch-all for names this file does not mention:
# customer domains that CNAME to this edge. Caddy mints a certificate
# per SNI at handshake time, gated by the ask in the global block, so
# a tenant hot-registering a new host gets TLS with no config reload
# and no wildcard. hub off because this site matches EVERY unknown
# host: leaving hub on here would make every on-demand name
# hub-enabled, defeating per-host hub admission. auth off for the same
# reason: an inherited global wall would demand a Janus login on every
# customer domain.
https:// {
import janus_access
tls {
on_demand
}
janus {
hub off
auth off
}
}