Playbook: LinkedIn Credential Rotation
Covers: LINKEDIN_ACCESS_TOKEN · LINKEDIN_CLIENT_ID · LINKEDIN_CLIENT_SECRET
Overview
LinkedIn OAuth credentials for personal brand automation (content publishing via n8n on bms-4).
| Key | SOPS file(s) | Purpose |
|---|---|---|
LINKEDIN_ACCESS_TOKEN | n8n-bms4 | Short-lived OAuth access token (~60 day lifetime) |
LINKEDIN_CLIENT_ID | n8n-bms4 + brandpilot | OAuth app Client ID (stable; changes only when app is recreated) |
LINKEDIN_CLIENT_SECRET | n8n-bms4 + brandpilot | OAuth app Client Secret |
LINKEDIN_PERSON_URN | n8n-bms4 | Author URN used as the author field on published posts |
LINKEDIN_TOKEN_EXPIRES | n8n-bms4 | Cached expiry of the current access token |
⚠️
brandpilot.env.sopsholds the SAME OAuth app credentials asn8n-bms4.env.sops. Verified 2026-07-19 by SHA-256 comparison:LINKEDIN_CLIENT_IDandLINKEDIN_CLIENT_SECRETare byte-identical across both files. Rotating the Client Secret inn8n-bms4alone will silently break BrandPilot’s LinkedIn publishing. Always update both files in the same PR.
Tier: ❌ Tier 3 Manual — OAuth 2.0 authorization requires browser login to linkedin.com.
When to rotate
LINKEDIN_ACCESS_TOKEN: Every ~60 days — LinkedIn enforces this expiry. Rotate when n8n publishing workflows fail with401 Unauthorized.LINKEDIN_CLIENT_SECRET: On exposure only.LINKEDIN_CLIENT_ID: Only if the OAuth application is recreated.
Pre-rotation checklist
- No LinkedIn publishing jobs running in n8n (wait for current job to finish)
- Browser available for OAuth consent flow
Rotation steps
Access token refresh (most common — every 60 days)
1. Complete OAuth flow (human required)
Option A — via n8n UI (preferred):
- n8n UI (
https://n8n.srv1072950.hstgr.cloud) → Credentials → find LinkedIn OAuth2 credential - Click Reconnect → authorize in the browser OAuth consent page → n8n stores the new token automatically
Option B — manual flow:
https://www.linkedin.com/developers/apps→ select the p24-infra app → Auth tab- Use the displayed OAuth 2.0 Authorization URL to initiate the consent flow in a browser
- After authorization, extract
access_tokenfrom the callback URL or token response - Store in
.env.localtemporarily — never paste in chat
2. Distribute (Claude handles)
If updated via n8n UI (Option A): only SOPS backup needs updating.
Update secrets/n8n-bms4.env.sops — key: LINKEDIN_ACCESS_TOKEN.
$env:SOPS_AGE_KEY_FILE = "C:\Users\konar\.age\p24-infra-keys.txt"
# Edit LINKEDIN_ACCESS_TOKEN per sops-edit-operations.md
sops --decrypt --input-type dotenv --output-type dotenv secrets\n8n-bms4.env.sops | Out-Null
if ($LASTEXITCODE -ne 0) { throw "SOPS corrupt — do NOT commit" }3. Verify
Trigger the LinkedIn publishing n8n workflow manually with a draft/test post.
Confirm 201 Created or 200 OK response from the LinkedIn API.
Non-destructive check of the token currently in SOPS — reads the real expiry straight from LinkedIn, so you can confirm whether a rotation is actually needed before doing one:
export SOPS_AGE_KEY_FILE=/home/claude-runner/.age/p24-infra-keys.txt
_li() { sops -d --input-type dotenv --output-type dotenv secrets/n8n-bms4.env.sops \
| grep "^$1=" | cut -d= -f2- | tr -d '"'; }
TOK=$(_li LINKEDIN_ACCESS_TOKEN)
# Liveness — 200 means the token still works
curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $TOK" \
https://api.linkedin.com/v2/userinfo
# Real expiry — never prints the token itself
curl -s -X POST https://www.linkedin.com/oauth/v2/introspectToken \
--data-urlencode "token=$TOK" \
--data-urlencode "client_id=$(_li LINKEDIN_CLIENT_ID)" \
--data-urlencode "client_secret=$(_li LINKEDIN_CLIENT_SECRET)" \
| python3 -c 'import json,sys,datetime as dt; d=json.load(sys.stdin); \
print(d.get("status"), dt.datetime.fromtimestamp(d["expires_at"], dt.timezone.utc).date())'
unset TOK4. Update the rotation registry — MANDATORY
Do not skip this step. credential-exporter has no way to see the real token expiry; it
computes overdue-ness as last_rotated + rotation_freq from dev_r_services. If you rotate the
token and leave the registry stale, CredentialRotationOverdue fires every few hours against a
perfectly healthy credential and each firing opens a fresh GitHub issue.
This is not hypothetical — it happened. A rotation on 2026-07-08 was not recorded, and the alert re-fired 13 times over the following 11 days (#3447 … #4302) while the live token was valid until 2026-09-06. See #4302 for the full trace.
Set last_rotated to the token’s real created_at and next_due to its real expires_at
(both from the introspection output above):
curl -s -X PATCH \
"${SUPABASE_URL}/rest/v1/dev_r_services?service_name=eq.LINKEDIN_OAUTH&element_type=eq.credential" \
-H "apikey: ${SUPABASE_SERVICE_KEY}" \
-H "Authorization: Bearer ${SUPABASE_SERVICE_KEY}" \
-H "Content-Type: application/json" \
-d '{"last_rotated":"YYYY-MM-DD","next_due":"YYYY-MM-DD"}'Then confirm p24_credential_days_overdue{credential="LINKEDIN_OAUTH"} returns to 0 on the
next credential-exporter scrape.
Client Secret rotation (on exposure)
1. Rotate in LinkedIn Developer Portal (human required)
https://www.linkedin.com/developers/apps→ select the app → Auth tab- Client Secret → Regenerate → copy new secret immediately
- Note: Client ID cannot be changed. If Client ID is also compromised, create a new app.
2. Distribute (Claude handles)
Update both secrets/n8n-bms4.env.sops and secrets/brandpilot.env.sops — key:
LINKEDIN_CLIENT_SECRET (they share one OAuth app — see the warning in §Overview).
Also update the n8n LinkedIn OAuth2 credential in the n8n UI with the new Client Secret.
Important: after changing the Client Secret, all existing access tokens become invalid. The OAuth flow must be re-completed (see Access Token Refresh above).
Log rotation
| YYYY-MM-DD HH:MM UTC | #ISSUE | LINKEDIN_ACCESS_TOKEN | scheduled 60d | radieu | n8n-bms4.env.sops + n8n vault |
Recovery
- OAuth flow fails (rate limit): wait 15 minutes and retry. Old token still valid until ~60d expiry.
- n8n credential reconnect fails: manually enter the access token via n8n Credentials UI.
- Token expired before rotation: complete the OAuth flow immediately; no rollback possible.
References
secret-rotation-access-matrix.md— Tier 3 classificationdocs/playbooks/sops-edit-operations.md— SOPS write pattern- #1525 — enable
offline_accessscope so the token can auto-refresh (durable fix; would retire the 60-day manual OAuth flow entirely)