paperclipai/paperclip · error

invalid_cloud_control_assertion

invalid_cloud_control_assertion

Error message

invalid_cloud_control_assertion

What it means

cloudControlMiddleware verified the request targets the correct endpoint, but verifyCloudControlAssertion rejected the compact JWS assertion — bad signature, expired token, wrong expected action, malformed JWS, or untrusted key. The middleware logs the rejection (without echoing the token) and returns 401 with code invalid_cloud_control_assertion, refusing to grant the 'paperclip-cloud' board actor.

Solutions

  1. Regenerate a fresh assertion from Paperclip Cloud and retry (rules out expiry)
  2. Ensure the assertion's action claim matches the HTTP method used (per ACTION_BY_METHOD)
  3. Verify instance/cloud clocks are synchronized (NTP) and the cloud signing keys are current on the instance
  4. Inspect server logs for the 'Rejected Cloud control assertion' warning to see the underlying verification error

Example fix

// before: reusing an old cached assertion
const assertion = cachedAssertion;
// after: mint per-request
const assertion = signCloudControlAssertion({ action: expectedAction, ttlSeconds: 60 });
Defensive patterns

Strategy: retry

Validate before calling

// before sending, mint a fresh, short-lived assertion for the exact action
const assertion = signCloudControlAssertion({ action: expectedAction, issuedAt: Date.now(), ttlSeconds: 60 });
if (decodeJwt(assertion).exp * 1000 < Date.now()) throw new Error("assertion already expired");

Try / catch

let res = await send(assertion);
if (res.status === 401 && res.body?.error === "invalid_cloud_control_assertion") {
  res = await send(signCloudControlAssertion({ action: expectedAction })); // one fresh-token retry
}

Prevention

When it happens

Trigger: An assertion sent to /api/instance/task-drain fails verifyCloudControlAssertion: signature mismatch, assertion expired, assertion's action claim does not match the expectedAction for the HTTP method, or the token is structurally invalid.

Common situations: Clock skew between cloud and instance causing expiry; the cloud signed the assertion for one action but the request used another method; key rotation on the cloud side not yet picked up by the instance; truncated/mangled assertion in a proxy or header.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/62c8a211d46e2d53. Report an issue: GitHub.

Appendix: source

Thrown at server/src/middleware/cloud-control.ts:47

  return (req, res, next) => {
    const assertion = req.get(CLOUD_CONTROL_HEADER)?.trim();
    if (!assertion) {
      next();
      return;
    }
    const expectedAction = ACTION_BY_METHOD[req.method];
    // Express's non-strict routing treats a trailing slash as the same
    // route; the endpoint check must agree with it.
    const normalizedPath = req.path.length > 1 && req.path.endsWith("/") ? req.path.slice(0, -1) : req.path;
    if (normalizedPath !== "/api/instance/task-drain" || !expectedAction) {
      res.status(400).json({ error: "cloud_control_wrong_endpoint" });
      return;
    }
    try {
      verifyCloudControlAssertion({ compactJws: assertion, expectedAction });
    } catch (error) {
      logger.warn({ err: error }, "Rejected Cloud control assertion");
      res.status(401).json({ error: "invalid_cloud_control_assertion" });
      return;
    }
    req.actor = {
      type: "board",
      userId: "paperclip-cloud",
      userName: "Paperclip Cloud",
      userEmail: null,
      isInstanceAdmin: true,
      source: "cloud_control",
    };
    next();
  };
}

View on GitHub (pinned to 3f1d897a7c)