Skip to content

Security

Yash Aryan edited this page Oct 9, 2026 · 3 revisions

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.

User authentication

  • Email + password, hashed (Argon2id / bcrypt). Session cookie cc_session: HMAC-SHA256, HttpOnly, SameSite=Lax, 7-day TTL. In production serve over TLS so Secure applies.
  • 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 (default viewer). 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).

Authorization (RBAC)

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/v1 require at least viewer.
  • 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.

Agent trust

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. AgentStream authenticates by SHA-256 fingerprint of the peer cert against servers.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_user requires 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.

Secrets

  • Encrypted at rest with AES-256-GCM envelope encryption. SECRETS_MASTER_KEY is 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.

Execution bounds

Each run:

  • Runs as run_as_user (default: the agent’s unprivileged user), in working_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.

Audit logging

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.

Two TLS surfaces (do not mix them)

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 model (brief)

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

Production hardening checklist

  • SESSION_SECRET and SECRETS_MASTER_KEY are 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).
  • /metrics firewalled, or gated with METRICS_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 (or warn) in production.

Clone this wiki locally