Skip to main content

MerlinCDN module

Uploads a certificate to MerlinCDN and binds it to one or more CDN distributions' canonical-names (CNAMEs). MerlinCDN certificates are immutable, so every renewal uploads a fresh certificate and re-binds.

Overview

  • Transport: MerlinCDN REST API (https://api.merlincdn.com by default; overridable per target).
  • Auth: a personal access token (Bearer) plus a merlin-organization-id and merlin-workspace-id header.
  • Certificates are immutable: there is no update-in-place. Each deployment uploads a new certificate (a new numeric id) and re-points a CNAME at it.
  • Rollback: supported — a rollback re-uploads a previous retained certificate version and re-binds the CNAME(s) to it (Rollback).

Prerequisites

  • A MerlinCDN CDN distribution and a canonical-name (CNAME) under it, or a domain the module can create the CNAME for.
  • A personal access token with certificate + canonical-name permissions, plus your organization and workspace ids.

Create the module credential

Settings → Distribution → Credentials → Add Credential → type MerlinCDN Token. Provide:

  • Personal Access Token — the Bearer token.
  • Organization ID — sent as merlin-organization-id.
  • Workspace ID — sent as merlin-workspace-id.

Stored as an envelope-encrypted JSON blob:

{
"personal_access_token": "<merlin PAT>",
"organization_id": "<merlin-organization-id>",
"workspace_id": "<merlin-workspace-id>"
}

Create a MerlinCDN target

Settings → Distribution → Targets → Add Target → type MerlinCDN:

  • API Host (optional) — defaults to https://api.merlincdn.com. Only https://api.merlincdn.com or a loopback/private address (for an on-prem mock; plain http is allowed only there) is accepted when the target is saved — the same allowlist the live pickers enforce, because every call sends the decrypted token to this host.
  • Bind to CNAME (default on) — off = upload-only mode (see below).
  • Distribution bindings (optional) — one row per CDN distribution to bind. Both fields are live pickers: the Distribution is chosen by its display name and the Canonical Name (CNAME) from that distribution's live list, fetched from the MerlinCDN API with the selected credential (no hand-typed ids). Rows may be left empty — a certificate link can supply its own rows via Target Overrides. (Legacy rows saved with a domain instead of a CNAME id keep working; picking a CNAME replaces the domain.)
  • Delete old certificate (default on) — remove the previous certificate after the re-binds. Guarded: before deleting, the whole workspace is scanned — a previous certificate still bound to any other CNAME (e.g. a distribution you never added as a binding row) is kept with a warning; if the scan fails or the workspace exceeds 50 distributions, deletion is skipped (kept) with a warning.

Execution flow

  1. Authenticate with the token + organization/workspace headers.
  2. Upload the certificate (POST /v1/cdn/certificates, multipart cert_file + key_file) → a new certificate id. If the upload fails nothing is bound. When the response carries no id, the id is recovered from the certificate list by the certificate's first domain (or, for a record without domains, the leaf's own CN/SAN); if it still cannot be recovered the target fails with MERLINCDN_UPLOAD and a warning that a stray upload may remain in the workspace — remove it in the panel.
  3. Resolve each binding row's CNAME: explicit canonical_name_id → previous run → match by domain. A domain-only row whose domain matches no CNAME creates one in that distribution (a typo therefore creates a wrong CNAME rather than failing).
  4. Bind: PATCH /v1/cdn/distributions/:id/canonical-names/:cnameId with the new certificate_id, once per binding row. If every bind fails and the certificate was uploaded in this run, that upload is deleted (orphan cleanup) — a reused upload is kept; if only some fail, the successes stay bound and a re-run heals the rest.
  5. If delete_old_cert is on and all binds succeeded, best-effort delete the previous certificates this renewal displaced. After a partial failure the displaced certificates are remembered in the target's state and deleted by the run that heals the last row.

Multiple distributions & upload-only mode

A target carries a list of binding rows (bindings: distribution_id + canonical_name_id or domain per row). On every deploy the certificate is uploaded once, then each row's CNAME is re-pointed at it — so a single target pushes the same certificate to several CDN distributions. The rows are defaults and may be left empty: each certificate link can replace the whole list via Target Overrides (bindings), so one target serves many certificates, each with its own set of distributions. If some binds fail, the successful ones stay bound and re-running the distribution heals only the missing rows — the already-uploaded certificate is reused, never re-uploaded.

Set bind: false for upload-only mode: the certificate is uploaded and you manage the CNAME binding manually in the MerlinCDN panel. In this mode any configured binding rows are ignored with a warning on every run, and delete_old_cert is ignored (the previous upload might be live in a manual binding). A rollback, like any deploy, only uploads — the binding stays operator-managed. Note: per-link Target Overrides can change the binding coordinates but cannot flip upload-only mode — bind is fixed at the target level.

Removing a binding row: the module never touches a CNAME it no longer manages. The removed row's CNAME keeps serving the previous certificate; the next run keeps that certificate (even with delete_old_cert on) and warns once — re-point or clean it up in the MerlinCDN panel, otherwise it serves an aging certificate until expiry.

Live-compare idempotency: every run compares each CNAME's live certificate_id on the CDN against the (re)used upload — already pointing at it ⇒ that binding is skipped (no PATCH); pointing elsewhere (a rollback, or a manual re-point in the panel) ⇒ it is re-bound, reusing the already-uploaded copy (no duplicate upload). Stored state is never trusted for the skip decision. A certificate displaced by such a drift repair is never deleted (it was bound outside CertAutoPilot's deploy and may serve other CNAMEs); only a certificate this renewal itself displaced — by a fresh upload, or recorded during a partially failed run of the same version — is a deletion candidate.

Health check authenticates and reads the first binding row's distribution only (GET /v1/cdn/distributions/:id); other rows are not probed.

Finding your organization & workspace ids: every API call carries them as the merlin-organization-id/merlin-workspace-id headers, but MerlinCDN's Postman docs inject them via a hidden pre-request script, so endpoint examples never show them. Copy the two header values from any panel request in browser DevTools → Network (app.merlincdn.com), or consult MerlinCDN's "API Usage" PDF / support.

Rollback

Supported via previous-version re-deploy: a rollback re-uploads a previous retained certificate version and re-binds every bound CNAME to it — the module's normal deploy path, fed older material. It no longer depends on delete_old_cert having been off: the previous version comes from CertAutoPilot's own artifact history, not from a certificate kept on the CDN. During a rollback run the delete_old_cert behavior is suppressed, so the certificate the rollback displaces stays uploaded. Rolling forward again is a normal deploy: the current version's fingerprint no longer matches the state, so it is uploaded again (a third object) and re-bound; with delete_old_cert on the rollback's upload is deleted if unreferenced, while the upload the rollback displaced is no longer tracked and is never deleted — expect one leftover certificate per rollback-and-forward cycle, to be removed in the panel. Eligibility, the version picker, and auto-rollback: Rollback.

Because execute uploads before it binds, an upload failure never changes the live CDN state.

Troubleshooting

  • MERLINCDN_AUTH — the token, organization id, or workspace id was rejected during the upload phase.
  • MERLINCDN_NOT_FOUND — HTTP 404 during the upload phase (typically a wrong host path). A missing distribution or canonical-name during binding surfaces as MERLINCDN_BIND.
  • MERLINCDN_UPLOAD — the certificate upload was rejected, or the new id could not be recovered (stray-upload warning).
  • MERLINCDN_BIND — a CNAME resolve/create/re-bind was rejected (a 4xx other than 429, a missing CNAME, an incomplete row); not retried — fix the row named in the message and re-run, successful rows stay bound and the upload is reused.
  • MERLINCDN_QUOTA / MERLINCDN_CONNECT — rate limited or a 5xx/connection failure, during the upload or when every failed binding row was transient (both retried).

See also