towerops/lib/towerops/vault.ex
Graham McIntire 97232117f5 fix: H12 cookie hardening + 5 low/medium bugs (L2, L5, L6, L8, L10, L11)
- H12: session and remember-me cookies get http_only + secure (prod-only).
  Cookie can no longer be read via document.cookie (XSS exfil defense)
  and the Secure flag is set in production via config/prod.exs.
- L2: 404 tracker uses EXPIRE … NX so a sustained probe can't keep
  refreshing the 60s window and dodge the threshold ban.
- L5: vault only reads CLOAK_KEY when :env == :prod — a developer with
  a prod env var set in their shell won't accidentally encrypt local
  data with the production key.
- L6: health endpoint no longer leaks the app version.
- L8: Preseem.dismiss_insight/2 + InsightsLive uses it — dismissing now
  requires the insight to belong to the user's org (closes IDOR).
- L10: SidebarCollapse JS hook stores its click handler and removes it
  in destroyed(); listeners no longer accumulate across LV navigation.
- L11: WebMCP navigate tool rejects anything that isn't a same-origin
  absolute path (blocks javascript:, data:, off-site URLs).
2026-05-12 11:22:47 -05:00

59 lines
1.5 KiB
Elixir

defmodule Towerops.Vault do
@moduledoc """
Encryption vault for sensitive data using Cloak.
Encrypts data using AES-256-GCM encryption. The encryption key is loaded
from the CLOAK_KEY environment variable (base64-encoded).
## Usage
Fields that use this vault are automatically encrypted/decrypted by Ecto.
See `Towerops.Encrypted.Binary` for the custom Ecto type.
## Key Generation
Generate a new encryption key:
openssl rand -base64 32
Store this key securely in 1Password and set it as the CLOAK_KEY
environment variable in production.
"""
use Cloak.Vault, otp_app: :towerops
@impl GenServer
def init(config) do
# In dev/test, use the key from config files so a developer who happens
# to have CLOAK_KEY set in their shell (e.g., from a prod-deploy env)
# doesn't accidentally encrypt local data with the production key.
# In production (runtime.exs), the env var override is required.
config =
if Application.get_env(:towerops, :env, :prod) == :prod and System.get_env("CLOAK_KEY") do
Keyword.put(config, :ciphers,
default: {
Cloak.Ciphers.AES.GCM,
tag: "AES.GCM.V1", key: decode_env!("CLOAK_KEY")
}
)
else
config
end
{:ok, config}
end
defp decode_env!(var) do
var
|> System.get_env()
|> case do
nil ->
raise """
Environment variable #{var} is missing.
Generate a key with: openssl rand -base64 32
"""
value ->
Base.decode64!(value)
end
end
end