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:
1 parent
6abd510628
commit
ddb0472f9b
9 files changed
+284
-37
No files matched your search
@@ -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
|
||||
`)
|
||||
}
|
||||
Reference in new issue
Block a user