6.4 KiB
Security Standards
Philosophy
Defense in depth, least privilege, fail closed. Authentication verifies who; authorization verifies what they can do. Centralize both — never duplicate permission checks across handlers.
RBAC Enforcement
All protected routes use RequirePermission middleware attached at registration time:
// In router/routers.go
hostsGroup.GET("", RequirePermission("host:view"), handler)
hostsGroup.POST("/control", RequirePermission("host:control"), handler)
RequirePermission in controllers/rbac_check.go resolves the user from the JWT, looks up their effective permissions (including inherited role permissions), and aborts with 403 if the required permission is absent.
cluster_admin bypasses all permission checks via early return — do not add new bypass paths outside this single mechanism.
Permission String Format
"action:scope" — e.g., "queue:submit", "host:view", "queue:control".
Scopes: all (unrestricted), self (own resources), cluster, hostgroup, self_ug, self_hg.
Permissions are defined in config/rbac.yaml. Roles support inheritance via includes: — resolution is recursive. Add new permissions to the YAML first, then wire them onto routes.
JWT Authentication
Token flow:
POST /auth/logon→ validates credentials via configured auth backend → issues JWT with{username, IP, expiry}claims- Every subsequent request:
Authorization: Bearer <token>header middleware/validatehandler.gostrips the prefix, parses with golang-jwt, validates the IP claim against the request IP
IP-bound tokens prevent token theft across network boundaries. Do not remove the IP validation.
Token secret is loaded from config/auth.yaml at startup. It must never appear in source code or logs.
Auth Backends
Backend selection is controlled by config/auth.yaml's provider: field, matched literally against a switch in backend/auth/init.go — an unrecognized value (e.g. configfile without the underscore) fails apiserver startup with "unsupported auth provider". Valid identifiers: config_file, pam, ldap, oauth2. Only config_file (dev mode, users defined in the YAML) and pam (Linux + CGO only) are functional today — ldap and oauth2 are unconditional stubs whose Authenticate() always returns "not yet implemented". All backends implement the same interface — adding a new backend means implementing that interface, not modifying middleware.
Input Validation
Validate at the API boundary inside preCheck{Resource}Request() helpers — before any service or store access. Return 400 with a clear message on validation failure. Do not pass unvalidated input to downstream layers.
Secrets
- JWT secret key:
config/auth.yaml— should be file-system protected (e.g. chmod 0600); this is not currently enforced by the code or deploy tooling, so treat it as an operator responsibility, not a guarantee - LDAP bind credentials:
config/auth.yaml - Never log tokens, passwords, secret keys, or full request bodies containing credentials
- Validate all required config at startup; fail fast with a clear error if missing
Rate Limiting and IP Whitelist
Rate limiting (middleware/rate_limit.go) and IP whitelist (middleware/whitelist.go, which implements both WhitelistURLMiddleware() and WhitelistIPMiddleware()) are applied before JWT validation in the middleware chain. They use ctx.ClientIP() and abort with 429 / 403 respectively. Both are configured in config/ YAML — do not hardcode thresholds.
Transport
All daemon-to-daemon communication is over TCP. The WAL and replication wire format uses Cap'n Proto — schema files in src/schema/. Do not add unvalidated deserialization paths for external input.
XCE uses server-side TLS only — encryption without peer certificate verification. There is no mTLS and no CA-pinning anywhere in the system; peer verification is deferred as optional P3 hardening (see bug-1453, which deleted all CA-cert/client-cert plumbing from both the C tier and the Go tier as dead code). Do not reintroduce CA-cert loading, client certificates, or cert-pinning without a design doc — this posture is intentional, not an oversight.
Clients talking to the API server (xcecli, or any HTTP client of apiserver) must support both http:// and https:// — the scheme is a per-deployment/user choice (APIServerInfo.URL), not something the client hardcodes. When the scheme is https://, set InsecureSkipVerify: true on the TLS client config (see xcecli/common/request/request.go's NewAPIClient) rather than attempting cert validation — the API server's cert is typically self-signed/auto-generated and the client has no CA to validate it against.
Clients talking to an XCE controller directly (worker, scheduler, and any other libxce-based client) need only server-side certs — no client-side certs are required or supported. The live connection path is xcecd_join_cluster() (src/worker/src/controller_connector.c) calling xcecli_start/xcecli_join (src/lib/client.c), which drives endpoint_connect() in src/lib/netio.c over an SSL_CTX built by create_ssl_context(NULL, NULL) (src/lib/xcectx.c) — note that cluster.c's do_connect()/xce_connect_cluster()/xce_reconnect_init/xce_reconnect_step are an unused stub path (explicitly documented as "to be wired in a later task") with no callers outside cluster.c and its own testsuite; don't point at them as the live mechanism. xce_cluster_config_t has no ca_cert_path/client-cert fields; do not add any. TLS verification mode is SSL_VERIFY_NONE throughout — this matches the API-server posture above, just at the C/libxce layer instead of Go/HTTP.
Known follow-up (not yet scoped): a HAProxy reverse proxy in front of apiserver is planned to terminate HTTPS, likely with a CA-trusted cert (Let's Encrypt candidate, unconfirmed), and whether apiserver's own RunTLS/httpsConfig.Enabled listener stays in the picture is also undecided. Once both are settled, revisit xcecli's blanket InsecureSkipVerify: true: if the client-facing endpoint ends up CA-trusted, default to real verification (Go's default tls.Config{} validates against the system trust store with no extra plumbing) and make skip-verify an explicit opt-in for self-signed deployments only — matching the automated security review's suggested fix. Don't implement this speculatively; the right shape depends on which of those two decisions lands where.