tutorial

How to Monitor Cloak (Elixir) with Vigilmon

Keep your Cloak-encrypted fields healthy in production — detect decryption failures, monitor key rotation events, and alert on vault misconfiguration before sensitive data becomes inaccessible.

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.Vault GenServer 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)

  1. Sign in at vigilmon.online
  2. Click New Monitor → HTTP
  3. URL: https://yourdomain.com/health
  4. Expected status: 200
  5. Add a keyword check for "vault":"ok" in the response body
  6. 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)

  1. Click New Monitor → Heartbeat
  2. Name: Cloak Key Rotation
  3. Expected interval: set to match your rotation schedule (e.g., 30 days)
  4. 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.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →