# `mix phoenix_kit.integrations.rotate_key`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.13.7/lib/mix/tasks/phoenix_kit.integrations.rotate_key.ex#L1)

Rotates the encryption key protecting stored integration credentials.

## Usage

    $ mix phoenix_kit.integrations.rotate_key --dry-run
    $ mix phoenix_kit.integrations.rotate_key
    $ mix phoenix_kit.integrations.rotate_key --new-key="<already-generated-secret>"

## What it does

1. Reads every stored integration connection.
2. Decrypts each one under whichever key is CURRENTLY active (a
   dedicated `:integrations_encryption_key` if configured, else the
   legacy `secret_key_base`-derived key — see
   `PhoenixKit.Integrations.Encryption`).
3. If every row decrypts cleanly, re-encrypts all of them under the new
   secret in a single database transaction — either every connection
   rotates, or (on any failure) none do.
4. Prints the new secret (unless you supplied one with `--new-key`) and
   the config to set.

This task does NOT write any config file or environment variable — you
must configure `integrations_encryption_key` yourself (typically from an
env var in `runtime.exs`) and restart the app.

## When to run this

  * **First adoption** — no dedicated key is configured yet, so every
    connection is protected only by the legacy `secret_key_base`-derived
    key. Run this once, set the printed secret, restart.
  * **Suspected key compromise** — a dedicated key is already configured.
    Run this, replace the env var with the newly printed secret, restart.
    Treat the old key as permanently compromised; do not reuse it.

## Options

  * `--dry-run` — runs the decrypt-and-verify pass over every row and
    reports how many WOULD rotate, without generating a key or writing
    anything. Unlike a real rotation, this does NOT take row locks —
    it's a plain read, safe to run against live traffic at any time, not
    just before committing to a real rotation.
  * `--new-key` — supply your own secret instead of generating one (e.g.
    one already stored in a secrets manager). Skipped in `--dry-run`. Must
    not be empty — `--new-key=""` is refused outright rather than
    silently falling back to a generated secret, since that's very likely
    a shell variable that resolved empty (`--new-key="$MAYBE_UNSET"`) and
    not something you meant to ask for.

## Run this with nothing else writing to integration connections

A real rotation's row lock only defends against ONE direction of a race
with a concurrent writer (an OAuth token auto-refresh, a "Test
Connection" click, ...) — see `PhoenixKit.Integrations.KeyRotation`'s
moduledoc, "Atomicity and concurrent writers", for the exact mechanism
and what it does NOT cover. In short: a writer that already read a row
before rotation locked it can still silently overwrite the freshly
rotated row with old-key content after rotation commits, and `rotate/2`
will have already reported success by then. **Pause anything that could
write to integration connections (most concretely: an OAuth token-refresh
worker) before running this for real, and do not resume it until you have
restarted the app under the new key** — not just until this command
returns. Treat the whole span, start to restart, as one maintenance
window.

## The gap between rotating and restarting

Rotation only changes what's in the database; the running app keeps using
the OLD key until you set the new one and restart. This is the SAME
maintenance window the section above requires — it doesn't end when this
command returns, it ends when the app is running under the new key. In
that window:

  * A READ of a rotated connection does not raise or crash — a field
    that can't be decrypted under the still-active old key is logged
    and silently dropped from whatever asked for it (see
    `PhoenixKit.Integrations.Encryption`'s decrypt-failure handling),
    same as any other decrypt failure. Not an exception to catch — the
    field is simply absent.
  * A WRITE that must first read-and-merge the existing row (most writes
    in `PhoenixKit.Integrations` work this way, including fully
    automatic ones like a validation-status update after every
    token-refresh attempt) is NOT destructive just because it hits a
    row this task already rotated: it restores the untouched field's
    ciphertext before saving, as long as the write itself doesn't
    supply a fresh value for that exact field. The field stays exactly
    as rotation left it and decrypts fine again once the app restarts.
  * A WRITE that DOES supply a fresh value for that exact field is
    still at risk, whether it's this simple gap or the race the section
    above describes: whatever gets encrypted uses whichever key is
    ACTIVE, which is still the OLD one until the app restarts, so that
    value lands under the OLD key in a row this task already moved to
    the new secret. It then fails to decrypt once the app restarts onto
    the new key, indistinguishable from unrelated corruption.

There is no dual-key fallback to paper over any of this (it would
silently mask exactly the failure class this task exists to prevent).
Restart promptly, and keep writers paused until you do.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
