A hardened install by default: baseline guardrails with Llama Guard 3 1B judging on a CPU, keys at most 90 days

server init (and the setup screens) write guard.default [baseline] —
secrets refused, cards masked, injection recorded, the judge refusing
S1-S4, S7, S9-S12 in prompts and answers, fail open after 5s — fetch the
judge into the mirror and run it on a node's CPU. --no-guard-model leaves
it out. The closing text names what init cannot do: TLS on the client
port and backup recipients.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilker ManapandClaude Opus 5.5 committed 2026-10-04 17:45:31 +02:00
1 parent 6abd510628
commit ddb0472f9b
9 files changed
+284 -37

No files matched your search

+87 -1
View File
@@ -81,6 +81,9 @@ type serverInit struct {
// make it serve something: a mirror, this machine as a node, and a small
// model on it.
noMirror, noNode, noModel bool
// noGuardModel leaves out the guardrails' judge model; the pattern
// checks in the baseline policy stay.
noGuardModel bool
// noFirewall leaves this machine's firewall, and the store members',
// as they are.
noFirewall bool
@@ -164,6 +167,8 @@ func newServerInitCmd() *cobra.Command {
"use the mirror on another machine, set up there by 'llmgw mirror init': its management API, http://host:8082")
f.BoolVar(&in.noNode, "no-node", false, "do not make this machine a node (and serve no model on it)")
f.BoolVar(&in.noModel, "no-model", false, "do not fetch and serve the starter model ("+starterName+")")
f.BoolVar(&in.noGuardModel, "no-guard-model", false,
"do not fetch and serve the guardrails' judge model ("+guardModelName+"); the pattern checks stay")
f.BoolVar(&in.noFirewall, "no-firewall", false,
"leave the firewall alone, instead of keeping llmgw's internal ports to the cluster's networks")
f.BoolVar(&in.postgres, "postgres", false,
@@ -226,6 +231,13 @@ func (in *serverInit) run(ctx context.Context) error {
return fmt.Errorf("starter model: %w", err)
}
}
if in.judgeWanted() && in.configJudgesWith(guardModelName) {
// Not fatal: without its judge the baseline policy still checks for
// secrets and injection, and a judged request is served flagged.
if err := in.ensureGuardModel(ctx); err != nil {
fmt.Fprintf(in.out, " !! guard model: %v; requests are not judged until it runs\n", err)
}
}
if hasSystemd && !in.noFirewall {
if err := in.ensureFirewall(ctx); err != nil {
// The cluster works without it; said, not fatal.
@@ -236,6 +248,70 @@ func (in *serverInit) run(ctx context.Context) error {
return nil
}
// judgeWanted is whether this install fetches the judge model, and so
// whether the baseline policy asks it.
func (in *serverInit) judgeWanted() bool {
if in.noGuardModel || (in.noMirror && in.mirrorURL == "") || in.unitPath == "" {
return false
}
_, err := exec.LookPath("systemctl")
return err == nil
}
// configJudgesWith is whether server.yaml has a policy asking that judge: a
// configuration written before init installed one is never changed, so the
// model is not fetched for nothing.
func (in *serverInit) configJudgesWith(model string) bool {
cfg, err := config.Load(in.configPath())
if err != nil {
return false
}
for _, p := range cfg.Guard.Policies {
if p.Judge != nil && p.Judge.Model == model {
return true
}
}
return false
}
// guardSection is server.yaml's guard: the baseline every request passes,
// with the judge model when there is one.
func guardSection(judge bool) string {
j := ` # A judge model reads what patterns cannot: violence, weapons, abuse,
# self-harm. Installed by init; --no-guard-model leaves it out.
# judge: {model: ` + guardModelName + `, input: block, output: block, ...}
`
if judge {
j = ` # A judge model reads what patterns cannot: violence, weapons, abuse,
# self-harm. It runs on a CPU, about half a second for a short text;
# an answer is read every 600 characters as it streams. Slower than
# latency_budget, the request is served and recorded as flagged.
# Defamation (S5), specialized advice (S6), intellectual property (S8)
# and elections (S13) are left out: they refuse too much ordinary work.
judge:
model: ` + guardModelName + `
input: block
output: block
categories: [S1, S2, S3, S4, S7, S9, S10, S11, S12]
latency_budget: 5s
`
}
return `# Guardrails (docs/guardrails.md). The baseline applies to every request;
# attach stricter policies to parts of the tree with 'llmgw ctl guard attach'.
# mode: shadow records what would be done without doing it.
guard:
default: [baseline]
policies:
baseline:
fail: open
input:
pii: {secret: block, card: redact}
injection: flag
output:
pii: {secret: block, card: redact}
` + j
}
func (in *serverInit) done(format string, a ...any) {
fmt.Fprintf(in.out, " ok "+format+"\n", a...)
in.changed = true
@@ -448,7 +524,7 @@ func (in *serverInit) ensureConfig(context.Context) error {
contact = "platform@example.com"
}
body := fmt.Sprintf(serverConfigTemplate, in.etcDir, in.etcDir, in.etcDir,
in.storeAddress(), contact)
in.storeAddress(), contact, guardSection(in.judgeWanted()))
if err := os.WriteFile(path, []byte(body), 0o640); err != nil {
return err
}
@@ -503,10 +579,14 @@ auth:
default_price:
prompt_per_1k: 1000
completion_per_1k: 1000
# The longest any API key lives; a key made without an expiry gets this.
# Shorter per organization, team or project with 'llmgw keys ttl'.
max_key_ttl: 2160h # 90 days
landing:
contact: "%s"
%s
# The engines every node installs for itself. Omitted, each gets the llama.cpp
# build this release was checked against.
# engines:
@@ -700,5 +780,11 @@ The control plane is up. Next:
llmgw ctl model add -f model.yaml
The web UI is on port 8080; /admin once you are signed in as the administrator.
Before people outside this network use it:
# port 8080 is plain HTTP: put a proxy with a certificate in front
# (deploy/proxy/ in the release; Caddy gets the certificate itself)
# backups carry every secret: name who may open them, backup_recipients
# in server.yaml, and copy them off this machine
`)
}