Skip to main content

Kick Webhook Public Key

Kick signs every webhook delivery with one RSA key. It publishes that key without authentication at https://api.kick.com/public/v1/public-key, in the shape {"data":{"public_key":"<SPKI PEM>"},"message":"OK"}. The API fetches, caches and refreshes this key itself. Operators do not set it by hand.

The refresher has the same shape as the InnerTube credential refresher, with one deliberate difference: it fails closed.

Resolution order​

webhooks.kick_public_key (optional pin) -> in-process key -> Redis -> live fetch from Kick
StageSourceNotes
1webhooks.kick_public_keyOptional operator pin. When set, the pin always wins, and the refresher and the rotation refetch are both off. If the pin cannot be parsed, every delivery is rejected. The fetched key is never used silently in place of a broken pin.
2In-process keyThis is the parsed key the receiver verifies against. An ordinary delivery never parses PEM and never reads Redis.
3Redis lumio:kick:webhook_public_keyHolds {pem, fetched_at} with a 24 h TTL. The leader writes it and every replica reads it.
4GET /public/v1/public-keyReturns the live key from Kick. A successful fetch is written to Redis and installed in the process.

There is no compiled-in fallback key. A baked-in key could not be revoked without a deploy if Kick ever rotated its key because of a compromise. When nothing resolves, the receiver answers 401 while webhooks.require_signatures = true (the default). The legacy webhooks.kick_secret HMAC path is consulted only in this no-key case.

Refresher worker​

apps/api/src/workers/kick_public_key.rs runs one cycle at startup and then one cycle every refresh_interval_secs:

  • Leader (Redis SET NX on lumio:kick:public_key_refresh:leader, TTL about 90 % of the interval, left to expire): fetches the key from Kick, writes Redis, and installs the key in its own process.
  • Followers: pull the Redis value into their own process.
  • Failed fetch: the key already held stays in use, because it is most likely still valid.
  • Cold boot: if a leader cycle ends with no usable key, the leader logs at ERROR and fires one Discord alert per incident. The alert is edge-triggered and latched in Redis under lumio:kick:public_key_cold_boot_alert, using the same state machine as the InnerTube alert (ZAF-206).

On-demand reload in the receiver​

POST /v1/webhooks/kick reloads the key itself in exactly two cases:

  1. Cold process. No key is held yet, for example when a delivery arrives before the refresher's first cycle. The receiver reads Redis, then fetches from Kick.
  2. Rotation. A signature fails against a fetched key (never against a pin). The receiver reloads once, checking Redis first because another replica may already hold the rotated key and then Kick. If the new key differs, it re-verifies once. It never loops.

Both reloads are single-flight (a Tokio mutex) and are skipped when the previous on-demand attempt was less than refetch_cooldown_secs ago, or when the failing key was itself fetched that recently. The cooldown is floored at 60 s. A flood of forged deliveries therefore costs Kick at most one request per cooldown window, and a bad signature against a fresh key is plain 401 with no refetch.

Configuration​

[webhooks.kick_public_key_observer] in apps/api/config/default.toml:

KeyENVDefaultMeaning
refresh_interval_secsLUMIO__WEBHOOKS__KICK_PUBLIC_KEY_OBSERVER__REFRESH_INTERVAL_SECS21600Scheduled refresh interval (6 h). Shorter than the 24 h Redis TTL.
refetch_cooldown_secsLUMIO__WEBHOOKS__KICK_PUBLIC_KEY_OBSERVER__REFETCH_COOLDOWN_SECS60Minimum gap between on-demand reloads. Values below 60 are raised to 60.
public_key_urlLUMIO__WEBHOOKS__KICK_PUBLIC_KEY_OBSERVER__PUBLIC_KEY_URLhttps://api.kick.com/public/v1/public-keyFetch endpoint. Override it only for tests or an egress proxy.
cold_boot_alert_webhook_urlLUMIO__WEBHOOKS__KICK_PUBLIC_KEY_OBSERVER__COLD_BOOT_ALERT_WEBHOOK_URLemptyDiscord webhook for the no-key alert. Empty disables the alert, but the ERROR log still fires.
cold_boot_alert_after_failuresLUMIO__WEBHOOKS__KICK_PUBLIC_KEY_OBSERVER__COLD_BOOT_ALERT_AFTER_FAILURES1Number of consecutive no-key cycles before the alert fires. 0 counts as 1.

Metrics​

MetricTypeLabelsMeaning
kick_webhook_public_key_refresh_totalcountertrigger (scheduled/on_demand), outcome (changed/unchanged/fetch_failed)Fetches against Kick
kick_webhook_public_key_age_secondsgauge-Seconds since the key in use was fetched. 0 means pinned, -1 means there is no key and deliveries are rejected. The value is computed at scrape time.

A value above 24 h means refreshes are failing. A value of -1 means Kick events are being dropped.

Audit​

This worker emits no audit event. Refreshing the key is internal bookkeeping with no actor and no tenant effect (see Audit Events, "do not emit").

Code​

FileRole
apps/api/src/services/kick_public_key.rsResolution order, in-process key, single-flight reload, age collector
apps/api/src/workers/kick_public_key.rsScheduled refresher, leader lock, cold-boot alert
crates/lo-kick-api/src/webhook.rsfetch_public_key, parse_public_key, verify_rsa_signature_with_key
apps/api/src/routes/webhooks.rsPOST /v1/webhooks/kick receiver