Files
Ilker ManapandClaude Opus 5 216fc9bb03 Let people sign up, be approved, and issue their own keys
Every key needed an operator with a shell. This adds the login system that
made that necessary, which is the thing I argued against building until it was
asked for — so it is built as if it will be attacked, because a public login
form is.

Signing up produces an account that can do nothing. Approval is what makes it
work and stays on the CLI: having a shell on the control plane is what makes
someone an administrator, and that is a smaller thing to get right than roles
and an admin UI that mints credentials.

The details that matter:

Passwords are argon2id, where a slow hash is correct — the input is chosen by
a human and has a dictionary, which is the opposite of an API key. Both kinds
of authentication now live in this codebase and they are in separate packages
so neither set of assumptions leaks into the other. Length is the only rule:
composition rules push people towards Password1! and cost more entropy than
they buy.

Signup never says whether an address already has an account, and sign-in
answers the same way for a wrong password and a missing account — verifying
against a dummy hash when there is no user, so a stopwatch cannot enumerate
accounts. Account state is checked after the password, not before, so guessing
an address reveals nothing about it. Guessing is bounded per address rather
than per IP: an attacker has many addresses to come from and one to guess
against, and limiting by IP locks out an office behind one NAT.

Sessions are server-side, so signing out and suspending take effect
immediately rather than whenever a token expires; suspension is checked in the
same query that resolves the session, because a second round trip is a check
somebody eventually forgets. Every state-changing form carries a CSRF token
verified against a hash in the session — a cookie authenticates the browser,
not the page that asked.

Key ownership is enforced in the UPDATE rather than by reading first, so a
guessed key id from another account changes nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 09:32:35 +02:00

51 lines
1.3 KiB
Go

// Package cli builds llmgw's command line surface.
package cli
import (
"log/slog"
"github.com/spf13/cobra"
"gitea.mynodes.xyz/ilker/llmgw/internal/obs"
"gitea.mynodes.xyz/ilker/llmgw/internal/version"
)
type globalFlags struct {
logLevel string
logFormat string
}
var global globalFlags
// Root returns the root command with all subcommands attached.
func Root() *cobra.Command {
root := &cobra.Command{
Use: "llmgw",
Short: "LLM gateway for multi-fabric InfiniBand and heterogeneous GPU clusters",
Long: "llmgw serves LLMs running on your own GPU servers behind a single\n" +
"OpenAI-compatible endpoint. One binary, different roles per subcommand.",
Version: version.Version,
SilenceUsage: true,
SilenceErrors: true,
PersistentPreRun: func(_ *cobra.Command, _ []string) {
obs.Setup(obs.Options{Level: global.logLevel, Format: global.logFormat})
},
}
pf := root.PersistentFlags()
pf.StringVar(&global.logLevel, "log-level", "info", "log level: debug|info|warn|error")
pf.StringVar(&global.logFormat, "log-format", "text", "log format: text|json")
root.AddCommand(
newServerCmd(),
newAgentCmd(),
newCtlCmd(),
newTenancyCmd(),
newUsersCmd(),
newSimEngineCmd(),
)
return root
}
func logger() *slog.Logger { return slog.Default() }