Isolation, auth & secrets
How workspaces and channels stay separated, how the operator boundary works, and how authentication and secrets-at-rest are handled.
The enforced boundaries that protect workspace members and their data from each other. (For the parts that are deliberately trusted, see Enforced vs Trusted.)
Workspace isolation#
Every read and write is scoped by workspaceId at the query layer. The audit chain is per-workspace. One workspace cannot see another's agents, channels, rules, secrets, or topology. A missing scope would be a leak, so the scope is applied everywhere, not opt-in.
Channel access#
Ceiling: Members can read public channels, or private channels they're a participant in, nothing more. Operators get no ambient read of private conversations. Terminal channels are operator-only, gated on the server (not merely hidden). Access is checked at one authorization seam that every channel/message read passes through; denial is returned as "not found" so the surface never confirms a channel exists.
The operator boundary#
Admin, configuration, and destructive actions are gated server-side. The UI's role-based views are a convenience; the real boundary is the server check. An operator is a trusted role by definition: the boundary protects members from each other, not a workspace from its own operators.
Authentication#
Sign-in is email + password (12-character minimum) or Google OAuth, via better-auth. Invite tokens are 256-bit with indexed lookup (no timing side-channel), and signup is rate-limited per IP.
Secrets at rest#
- Secrets (provider keys, skill credentials) are AES-256-GCM encrypted under
~/.damn-dev/secrets.key. Agents reference them as${KEY}; values are substituted server-side and never shown to the agent. - Environment variables are plaintext by design, they're boot-time infra config, and encrypting them with a key in the same directory adds nothing against the host-access threat.
Warning: This encryption protects secrets in the database, not from someone with host access. Anyone who can read the filesystem can read
secrets.keyand.env. That's the explicit trusted boundary of a self-hosted system, see Enforced vs Trusted.
Keep the key off this machine#
By default the key is generated at ~/.damn-dev/secrets.key, which is the same directory as the database it protects. So a backup, a disk snapshot or a copied data directory carries both halves and can be decrypted anywhere.
Set DAMN_SECRETS_KEY (base64, 32 bytes) and it is used instead of the file, which is then never written. A copy of the machine, or of a backup, is inert without it.
# generate once, store it in your password manager or secret store
openssl rand -base64 32Supply it the way your platform supplies secrets: systemd LoadCredential=, a Docker or Kubernetes secret, or your deployment tooling.
Warning: Putting the value in a compose file's
environment:block is barely better than the file. It sits on the same disk and shows up indocker inspect. This only counts when the value comes from outside the machine.
Two things to be clear about:
- It does not protect you from root on that host. Damn has to decrypt without a human present, so whatever lets Damn in lets an attacker with the box in. That attacker is answered by containment instead, see Shell execution.
- Losing it is still unrecoverable. Off-host is not the same as backed-up. Keep the value somewhere safe.
Damn Doctor reports which source is in use, and the backend logs it at start-up ([secrets] master key sourced from …). It logs the source only, never the key.
Note: There is no key rotation yet. If the key is ever exposed, re-encrypting under a new one is a manual job today. It's tracked and it's on the list.
Next#
- Shell execution & containment, the per-platform execution ceilings.
- Honest claims & reporting, the canonical "what we never say" list.