How to Monitor Cloak (Elixir) with Vigilmon
Cloak is a transparent field-level encryption library for Elixir and Ecto. It wraps Ecto types — Cloak.Ecto.Binary, Cloak.Ecto.SHA256, Cloak.Ecto.NaiveDatetime — so that values are encrypted on write and decrypted on read without any change to your query interface. A Cloak.Vault holds your cipher configuration and key material, supporting multiple keys simultaneously for zero-downtime key rotation.
When Cloak works, sensitive columns (SSNs, API tokens, PII) are ciphertext in the database and plaintext in your application — transparently. When something breaks — a vault is misconfigured after a deploy, a key is rotated but old ciphertext is still being read, or an environment variable carrying the key material is missing in production — decryption silently raises or returns garbage. Vigilmon monitors catch vault problems before users encounter corrupt data.
Why Monitor Cloak?
Encryption failures are among the worst-hidden bugs in web applications:
- Missing key in production — a deploy adds a new Cloak vault cipher but the corresponding environment variable isn't set; every read of an affected column raises
Cloak.MissingKeyError - Key rotation left old ciphertext unreadable — rotating keys without migrating existing rows means records written before the rotation fail to decrypt
- Vault module not started — if the
Cloak.VaultGenServer isn't in your supervision tree after a restart, all vault operations raise immediately - Algorithm mismatch after a library upgrade — upgrading Cloak changes default ciphers; existing ciphertext may be unreadable with the new defaults
- Wrong environment variables across stages — staging uses a different key than production; data copied between environments fails to decrypt
Key Metrics to Track
| Metric | What it tells you |
|--------|-------------------|
| Vault health (startup) | Whether MyApp.Vault is running and can encrypt/decrypt a test value |
| Decryption error rate | Number of Cloak.MissingKeyError or CryptoError exceptions per minute |
| Key version distribution | Whether any rows still hold ciphertext for a retired key version |
| Rotation job freshness | Whether the background key-rotation migration ran successfully |
| Health endpoint status | Whether the app can serve requests that involve encrypted fields |
Step 1: Add Cloak to Your Project
# mix.exs
defp deps do
[
{:cloak, "~> 1.1"},
{:cloak_ecto, "~> 1.3"},
# rest of deps
]
end
Define your vault:
# lib/my_app/vault.ex
defmodule MyApp.Vault do
use Cloak.Vault, otp_app: :my_app
end
Configure it:
# config/runtime.exs
config :my_app, MyApp.Vault,
ciphers: [
default: {
Cloak.Ciphers.AES.GCM,
tag: "AES.GCM.V1",
key: Base.decode64!(System.fetch_env!("CLOAK_KEY_V1")),
iv_length: 12
}
]
Add the vault to your supervision tree:
# lib/my_app/application.ex
children = [
MyApp.Repo,
MyApp.Vault, # must be before any process that reads encrypted fields
MyAppWeb.Endpoint,
# ...
]
Step 2: Write a Vault Health Check
# lib/my_app/vault_health.ex
defmodule MyApp.VaultHealth do
@doc """
Returns :ok if the vault can round-trip encrypt/decrypt, or {:error, reason}.
Safe to call from a health endpoint — uses a fixed test plaintext, no DB access.
"""
def check do
plaintext = "vigilmon-vault-health-check"
with {:ok, ciphertext} <- MyApp.Vault.encrypt(plaintext),
{:ok, ^plaintext} <- MyApp.Vault.decrypt(ciphertext) do
:ok
else
{:error, reason} -> {:error, reason}
{:ok, wrong} -> {:error, {:decrypt_mismatch, wrong}}
end
rescue
e -> {:error, Exception.message(e)}
end
end
Expose it in your health endpoint:
# lib/my_app_web/controllers/health_controller.ex
defmodule MyAppWeb.HealthController do
use MyAppWeb, :controller
def check(conn, _params) do
checks = %{
database: check_database(),
vault: check_vault()
}
status = if Enum.all?(checks, fn {_, v} -> v == :ok end), do: 200, else: 503
conn
|> put_status(status)
|> json(%{status: if(status == 200, do: "ok", else: "degraded"), checks: checks})
end
defp check_database do
case Ecto.Adapters.SQL.query(MyApp.Repo, "SELECT 1", []) do
{:ok, _} -> :ok
{:error, _} -> :error
end
end
defp check_vault do
case MyApp.VaultHealth.check() do
:ok -> :ok
_ -> :error
end
end
end
Test it locally:
curl http://localhost:4000/health
# {"status":"ok","checks":{"database":"ok","vault":"ok"}}
If the CLOAK_KEY_V1 environment variable is missing or malformed, the vault check returns "vault":"error" and the endpoint returns 503.
Step 3: Log Decryption Errors in Production
Cloak raises on decryption failure. Catch and count them at the point they happen by adding error logging to your Ecto schema callbacks or a telemetry handler:
# lib/my_app/schemas/user.ex
defmodule MyApp.Schemas.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :email, MyApp.EncryptedField
field :ssn, MyApp.EncryptedField
timestamps()
end
def changeset(user, attrs) do
user
|> cast(attrs, [:email, :ssn])
|> validate_required([:email])
end
end
# lib/my_app/encrypted_field.ex
defmodule MyApp.EncryptedField do
use Cloak.Ecto.Binary, vault: MyApp.Vault
require Logger
# Override load/1 to capture decryption errors
def load(value) do
case super(value) do
{:ok, plaintext} ->
{:ok, plaintext}
{:error, reason} = error ->
Logger.error("[Cloak] Decryption failed", reason: inspect(reason))
:telemetry.execute(
[:my_app, :cloak, :decrypt_error],
%{count: 1},
%{reason: reason}
)
error
end
end
end
Step 4: Monitor Key Rotation with a Heartbeat
Key rotation is a background task. Write a Mix task that migrates rows to the new key and pings a Vigilmon heartbeat on success:
# lib/mix/tasks/rotate_cloak_keys.ex
defmodule Mix.Tasks.RotateCloakKeys do
use Mix.Task
require Logger
@shortdoc "Re-encrypt all Cloak-protected fields with the current default key"
def run(_args) do
Application.ensure_all_started(:my_app)
Logger.info("Starting Cloak key rotation...")
rotated =
MyApp.Repo.transaction(fn ->
MyApp.Repo.all(MyApp.Schemas.User)
|> Enum.reduce(0, fn user, count ->
user
|> Ecto.Changeset.change()
|> MyApp.Repo.update!()
count + 1
end)
end)
case rotated do
{:ok, count} ->
Logger.info("Cloak rotation complete: #{count} rows re-encrypted")
ping_rotation_heartbeat()
{:error, reason} ->
Logger.error("Cloak rotation failed: #{inspect(reason)}")
System.halt(1)
end
end
defp ping_rotation_heartbeat do
url = System.get_env("VIGILMON_CLOAK_ROTATION_HEARTBEAT_URL")
if url do
case Req.get(url, receive_timeout: 10_000) do
{:ok, _} -> Logger.info("Vigilmon heartbeat pinged")
{:error, e} -> Logger.warning("Vigilmon heartbeat failed: #{inspect(e)}")
end
end
end
end
Run it after every key rotation:
mix rotate_cloak_keys
# Starting Cloak key rotation...
# Cloak rotation complete: 14832 rows re-encrypted
# Vigilmon heartbeat pinged
Step 5: Set Up Monitoring in Vigilmon
HTTP Monitor (vault health endpoint)
- Sign in at vigilmon.online
- Click New Monitor → HTTP
- URL:
https://yourdomain.com/health - Expected status:
200 - Add a keyword check for
"vault":"ok"in the response body - Check interval: 1 minute
The keyword check catches the case where the response is 200 but the vault check degraded — so you get alerted on partial health, not just full outages.
Heartbeat Monitor (key rotation job)
- Click New Monitor → Heartbeat
- Name:
Cloak Key Rotation - Expected interval: set to match your rotation schedule (e.g., 30 days)
- Copy the URL → set as
VIGILMON_CLOAK_ROTATION_HEARTBEAT_URL
Step 6: Alerting
In Vigilmon, go to Notifications → New Channel → Slack:
Vault health degraded:
🔴 DOWN: yourdomain.com/health
Keyword "vault":"ok" not found in response body
Action: Check CLOAK_KEY_V1 environment variable in production
Key rotation missed:
🔴 MISSED: Cloak Key Rotation
Last successful rotation: 35 days ago
Action: Run mix rotate_cloak_keys on the production release
What You Built
| What | How |
|------|-----|
| Vault health check | VaultHealth.check/0 — encrypt/decrypt round-trip |
| Health endpoint | /health with vault check field |
| Decryption error logging | Custom EncryptedField.load/1 override with telemetry |
| Key rotation task | mix rotate_cloak_keys with heartbeat on success |
| HTTP monitor | Vigilmon checks /health with keyword match every minute |
| Rotation heartbeat | Vigilmon alerts when rotation job misses its schedule |
| Slack alerting | Vigilmon Slack notification channel |
Cloak makes encryption invisible during normal operation. Vigilmon makes vault failures visible before users encounter them.
Next Steps
- Export
[:my_app, :cloak, :decrypt_error]telemetry to your APM to track decryption failure rates over time - Add a Vigilmon status page for your security team showing vault health and rotation cadence
- Set up a separate heartbeat for each Cloak-encrypted schema to know which model's keys need rotation
- Alert on decryption error rate spikes using Vigilmon's response body keyword matching on a metrics endpoint
Get started free at vigilmon.online.