tutorial

How to Monitor Cloak.Ecto with Vigilmon

Cloak.Ecto provides transparent field-level encryption in Elixir — but encryption key misconfiguration causes complete data loss, not just degraded performance. Here's how to monitor Cloak.Ecto-powered applications with Vigilmon to catch cryptographic failures before they become incidents.

How to Monitor Cloak.Ecto with Vigilmon

Cloak.Ecto provides Ecto types that transparently encrypt and decrypt database fields using the Cloak cryptography library. You annotate your schema fields with types like Cloak.Ecto.Binary or Cloak.Ecto.SHA256, and Cloak handles AES-GCM encryption on write and decryption on read — no changes to your query or changeset logic.

The monitoring challenge: encryption failures are catastrophic and silent. If a key rotation goes wrong or the vault configuration drifts, every database read for an encrypted field starts returning decryption errors. Depending on your error handling, this might surface as application errors, blank fields, or complete crashes — and none of these generate a useful uptime alert by default.

This tutorial sets up monitoring for a Cloak.Ecto-powered application:

  • A health endpoint that validates the encryption/decryption round-trip
  • HTTP uptime monitoring with Vigilmon
  • Heartbeat monitoring tied to key rotation events
  • Alerts on cryptographic failures

Why Monitor Cloak.Ecto?

| Signal | What it catches | |---|---| | Encryption round-trip health | Key misconfiguration, vault unavailability | | Key rotation status | Failed key rotations that leave stale ciphertext | | Decryption error rate | Ciphertext corruption, version mismatch | | Application availability | OTP crashes caused by vault startup failures | | Database connectivity | Ecto repo unavailability affecting encrypted reads |

Cloak.Ecto failures are not recoverable at runtime — if the key is wrong, the data is unreadable. You want a monitor that fires the moment this state is entered.


Step 1: Configure Cloak.Ecto

Set up your vault and annotate your schema:

# lib/my_app/vault.ex
defmodule MyApp.Vault do
  use Cloak.Vault, otp_app: :my_app
end
# config/config.exs
config :my_app, MyApp.Vault,
  ciphers: [
    default: {Cloak.Ciphers.AES.GCM, tag: "AES.GCM.V1", key: Base.decode64!("your_base64_key")}
  ]

Annotate your Ecto schema:

# lib/my_app/accounts/user.ex
defmodule MyApp.Accounts.User do
  use Ecto.Schema

  schema "users" do
    field :name, :string
    field :email, MyApp.Encrypted.Binary
    field :email_hash, MyApp.Hashed.Binary
    field :ssn, MyApp.Encrypted.Binary
    timestamps()
  end
end

Define your encrypted types:

# lib/my_app/encrypted/binary.ex
defmodule MyApp.Encrypted.Binary do
  use Cloak.Ecto.Binary, vault: MyApp.Vault
end

# lib/my_app/hashed/binary.ex
defmodule MyApp.Hashed.Binary do
  use Cloak.Ecto.SHA256
end

Step 2: Add an Encryption Health Check Endpoint

Create a health endpoint that performs a live encrypt/decrypt round-trip against your vault configuration:

# lib/my_app_web/controllers/health_controller.ex
defmodule MyAppWeb.HealthController do
  use MyAppWeb, :controller

  @test_plaintext "vigilmon-health-check-2026"

  def index(conn, _params) do
    checks = %{
      vault: check_vault(),
      database: check_database(),
      encryption_roundtrip: check_encryption()
    }

    all_ok = Enum.all?(checks, fn {_k, v} -> v == :ok end)
    status = if all_ok, do: "ok", else: "error"
    http_status = if all_ok, do: 200, else: 503

    conn
    |> put_status(http_status)
    |> json(%{
      status: status,
      checks: Map.new(checks, fn {k, v} -> {k, Atom.to_string(v)} end),
      timestamp: DateTime.utc_now() |> DateTime.to_iso8601()
    })
  end

  defp check_vault do
    case MyApp.Vault.encrypt(@test_plaintext) do
      {:ok, _ciphertext} -> :ok
      {:error, _} -> :error
    end
  rescue
    _ -> :error
  end

  defp check_encryption do
    with {:ok, ciphertext} <- MyApp.Vault.encrypt(@test_plaintext),
         {:ok, plaintext} <- MyApp.Vault.decrypt(ciphertext),
         true <- plaintext == @test_plaintext do
      :ok
    else
      _ -> :error
    end
  rescue
    _ -> :error
  end

  defp check_database do
    case MyApp.Repo.query("SELECT 1") do
      {:ok, _} -> :ok
      {:error, _} -> :error
    end
  rescue
    _ -> :error
  end
end

Add the route:

# lib/my_app_web/router.ex
scope "/", MyAppWeb do
  pipe_through :api
  get "/health", HealthController, :index
end

Test it:

curl http://localhost:4000/health
# => {"status":"ok","checks":{"vault":"ok","database":"ok","encryption_roundtrip":"ok"},...}

The endpoint returns 503 if the vault key is wrong, the vault process is down, or the encrypt/decrypt round-trip fails. This is the most important monitor you can add for a Cloak.Ecto application.


Step 3: Add Vigilmon HTTP Monitoring

  1. Log in to vigilmon.online and click Add Monitor.
  2. Set Type to HTTP / HTTPS.
  3. Enter your health endpoint: https://yourapp.com/health.
  4. Set Check interval to 1 minute.
  5. Set Expected HTTP status to 200.
  6. Under Advanced, add Response body contains: "encryption_roundtrip":"ok".
  7. Click Save.

This monitor is your first line of defense against key configuration problems. Set the alert threshold to 1 consecutive failure — encryption failures require immediate response.


Step 4: Monitor Key Rotation with a Heartbeat

Key rotation is the most error-prone operation in a Cloak.Ecto deployment. Add a post-rotation validation step that pings a Vigilmon heartbeat only if rotation succeeded:

# lib/mix/tasks/cloak_rotate.ex
defmodule Mix.Tasks.CloakRotate do
  use Mix.Task

  @shortdoc "Rotate Cloak.Ecto encrypted fields and validate"
  def run(_args) do
    Mix.Task.run("app.start")

    IO.puts("Starting key rotation for User.email...")
    count = rotate_user_emails()
    IO.puts("Rotated #{count} records")

    IO.puts("Validating encryption round-trip post-rotation...")
    case validate_post_rotation() do
      :ok ->
        IO.puts("Validation passed — pinging Vigilmon")
        ping_heartbeat()
      {:error, reason} ->
        IO.puts("VALIDATION FAILED: #{reason}")
        System.halt(1)
    end
  end

  defp rotate_user_emails do
    MyApp.Repo.all(MyApp.Accounts.User)
    |> Enum.reduce(0, fn user, count ->
      case MyApp.Accounts.update_user(user, %{email: user.email}) do
        {:ok, _} -> count + 1
        {:error, _} -> count
      end
    end)
  end

  defp validate_post_rotation do
    test_plaintext = "rotate-check-#{:os.system_time(:second)}"
    with {:ok, ct} <- MyApp.Vault.encrypt(test_plaintext),
         {:ok, pt} <- MyApp.Vault.decrypt(ct),
         true <- pt == test_plaintext do
      :ok
    else
      _ -> {:error, "encrypt/decrypt round-trip failed after rotation"}
    end
  end

  defp ping_heartbeat do
    url = System.get_env("VIGILMON_ROTATION_HEARTBEAT_URL")
    if url do
      :httpc.request(:get, {String.to_charlist(url), []}, [], [])
    end
  end
end

Run this as part of your key rotation workflow:

VIGILMON_ROTATION_HEARTBEAT_URL=https://vigilmon.online/api/heartbeat/YOUR_ID \
  mix cloak_rotate

To create the rotation heartbeat in Vigilmon:

  1. Click Add MonitorCron / Heartbeat.
  2. Set expected interval to match your rotation schedule (e.g., 30 days).
  3. Set grace period to allow for maintenance windows.
  4. Copy the heartbeat URL to VIGILMON_ROTATION_HEARTBEAT_URL.

If a rotation is missed or fails, Vigilmon alerts you — preventing the scenario where an old key expires without a valid replacement in place.


Step 5: Add Decryption Error Tracking

Track decryption errors in your application logic to catch individual field corruption:

# lib/my_app/encrypted/binary.ex
defmodule MyApp.Encrypted.Binary do
  use Cloak.Ecto.Binary, vault: MyApp.Vault

  def cast(value) do
    result = super(value)
    case result do
      {:error, _} ->
        :telemetry.execute([:my_app, :cloak, :decrypt_error], %{count: 1}, %{type: :cast})
        result
      _ -> result
    end
  end
end

Attach a telemetry handler to count errors:

:telemetry.attach(
  "cloak-decrypt-error",
  [:my_app, :cloak, :decrypt_error],
  fn _event, %{count: count}, _meta, _config ->
    MyApp.DecryptMetrics.increment(count)
  end,
  nil
)

Expose this counter in your health endpoint for deeper inspection.


Step 6: Set Up Alerts

Configure alert channels with appropriate severity:

  1. Go to Alert ChannelsAdd Channel.
  2. Set up a PagerDuty channel for encryption failures (high severity — data may be unreadable).
  3. Set up a Slack channel for rotation heartbeat misses (scheduled reminders).
  4. For the HTTP monitor, set threshold to 1 consecutive failure — encryption failures need immediate response.
  5. For the rotation heartbeat, set Missing heartbeat to alert after the grace period expires.

Also set up SSL certificate monitoring — certificate expiry can disrupt TLS-wrapped key delivery services:

  1. Click Add MonitorSSL Certificate.
  2. Enter your domain.
  3. Alert when the certificate expires in < 30 days (longer lead time for crypto infrastructure).

Key Metrics to Watch

| Metric | Vigilmon feature | What to alert on | |---|---|---| | Vault availability | HTTP monitor body check | "vault":"error" | | Encryption round-trip | HTTP monitor body check | "encryption_roundtrip":"error" | | Database connectivity | HTTP monitor body check | "database":"error" | | Key rotation freshness | Heartbeat monitor | Any missed rotation heartbeat | | Application availability | HTTP monitor on /health | Any 5xx or timeout | | SSL certificate | Cert expiry monitor | Expires in < 30 days |


Conclusion

Cloak.Ecto makes field-level encryption transparent to your application code — but that transparency cuts both ways. When encryption breaks, the failure is as invisible as the feature itself. An encrypt/decrypt health check that runs every minute and a rotation heartbeat that fires after every key change give you two layers of protection: catch configuration failures immediately, and catch missed rotations before they become emergencies.

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 →