Repository navigation
Security
Related ADRs: ADR-002 — Outbound mTLS gRPC (not inbound SSH / WebSocket-first) · ADR-005 — Secrets: envelope encryption, write-only API, inject at sync · ADR-008 — Agent root is explicit and narrow
CronCompose runs arbitrary shell scripts on the operator’s servers. Security is core, not an afterthought.
- Email + password, hashed (Argon2id / bcrypt). Session cookie
cc_session: HMAC-SHA256, HttpOnly, SameSite=Lax, 7-day TTL. In production serve over TLS soSecureapplies. - API tokens: bearer, stored only as a hash, plaintext shown once at creation, revocable.
- Optional OIDC SSO: auto-provisions users by email with
OIDC_DEFAULT_ROLE(defaultviewer). Password login stays available. See Operations.
Open endpoints (no user session): /healthz, /api/v1/auth/login, /api/v1/agents/enroll (one-time token of its own).
| Role | Can |
|---|---|
| viewer | Read jobs, runs, logs, connector status |
| operator | Create/edit/run jobs, connector lifecycle actions |
| admin | Manage servers, secrets, tokens, connector config, notification targets |
| owner | Everything, including user management |
Comparison is numeric rank: a route gated at operator admits operator/admin/owner.
Typical gating:
- All authenticated GETs under
/api/v1require at leastviewer. - Job mutations and run-now require
operator. - Server mutations (create, edit, delete, issue token) require
admin. - Web terminal is admin/owner only.
- Secret values are never returned by any endpoint, regardless of role.
Highest-risk boundary: an agent runs whatever scripts the control plane sends.
- Enrollment uses a one-time, short-TTL token, stored only as a hash, exchanged exactly once for a client certificate.
-
Steady state is mTLS. The control plane verifies the client cert (
RequireAndVerifyClientCert); the agent verifies the server cert.AgentStreamauthenticates by SHA-256 fingerprint of the peer cert againstservers.cert_fingerprint. - Revocation clears the fingerprint and deny-lists it; the agent must re-enroll.
- Direction: agents only ever dial out. No CronCompose ports need to be open on the target.
- Run the agent as a dedicated unprivileged system user, not root, unless a specific job’s
run_as_userrequires it.
The control plane bootstraps a self-signed CA + server cert under TLS_DIR on first start. Production can overwrite those files with a real PKI; the process loads whatever is on disk. Persist TLS_DIR. Regenerating the CA orphans every enrolled agent.
- Encrypted at rest with AES-256-GCM envelope encryption.
SECRETS_MASTER_KEYis a 32-byte hex key (64 hex chars). Back it up offline; losing it makes every stored secret unrecoverable. - Decrypt only at sync time; send over the already-encrypted mTLS stream.
- On the agent, secrets have restrictive file perms and are passed as environment variables at exec — never interpolated into the script body, never written to logs (known values are scrubbed).
- API is write-only for values.
Each run:
- Runs as
run_as_user(default: the agent’s unprivileged user), inworking_dir. - Bounded by
timeout_seconds; the whole process group is killed on timeout. - Honors
concurrency_policy(skip/allow/queue). - Optional systemd-run CPU/memory/process/IO limits.
Honest caveat: a job is arbitrary code on the user’s own machine. CronCompose limits who can define jobs (RBAC), proves which control plane and agent are talking (mTLS), and bounds how a job runs. It does not fully sandbox hostile code.
Mutating actions (create/edit/delete server, job, secret, token; manual run; cancel; enroll; revoke; terminal open/close; connector ops) write audit_log with actor, action, target, and metadata.
| Surface | How to terminate |
|---|---|
| Browser / REST (HTTP port) | Reverse proxy or load balancer in front of HTTP_ADDR. Control plane speaks cleartext HTTP. |
| Agents (gRPC port) | End-to-end mTLS. L4/TCP passthrough only. Never HTTP-terminate. |
| Threat | Mitigation |
|---|---|
| Stolen enrollment token | One-time use, short TTL, hash-only storage |
| Compromised agent cert | Revoke + deny-list, force re-enroll |
| Eavesdropping on agent channel | mTLS on the gRPC stream |
| Secret exposure at rest | AES-GCM; write-only API |
| Secret exposure in logs | Scrubbing; secrets via env |
| Privilege escalation via jobs | Unprivileged default; explicit run_as_user
|
| Malicious insider editing jobs | RBAC + audit log |
| Inbound attack on home servers | Agents dial out only |
| Runaway job | Timeout, concurrency, optional cgroups |
| Unverified agent self-update | Updates require a pinned SHA-256; no unverified path |
-
SESSION_SECRETandSECRETS_MASTER_KEYare real random 32-byte hex, not the dev defaults. Master key backed up offline. - TLS in front of HTTP; gRPC on TCP passthrough.
-
SEED_ADMIN_*strategy: either keep them as the managed source of truth, or unset after first login so UI-managed passwords persist (they re-apply on every boot). -
/metricsfirewalled, or gated withMETRICS_TOKEN. - Persistent volumes for Postgres and
TLS_DIR, both backed up. - Agents run as a non-root system user with the packaged systemd unit.
-
LOG_LEVEL=info(orwarn) in production.