Playbook: NEXCON_API_KEY Rotation

Service: Nexcon — SIM card provider for GPS trackers Secret: NEXCON_API_KEY Rotation frequency: 90 days (or immediately on suspected exposure) Last rotated: 2026-06-27 Next due: 2026-09-27


What uses this credential

ConsumerHowEffect if missing
nexcon-exporter container (vps-i1)REST API calls to Nexcon portal — SIM status, data usage metricsGPS tracker SIM metrics missing from Prometheus / Grafana
scripts/nexcon-*.py (if any)Direct API callsScript failures
Alertmanager rules for SIM healthDownstream — alert suppressed if exporter is downNo alerts on SIM disconnections

Where stored

LocationKey nameHow to update
secrets/monitoring.env.sopsNEXCON_API_KEYSOPS write pattern (see below)
.env.local on dev workstationNEXCON_API_KEYEdit line directly (emergency fallback only)
vps-i1 deployed env/opt/p24-infra/monitoring/.envAuto-synced by secrets-sync.yml on merge to dev/main

Nexcon portal URL

The Nexcon customer portal base URL is stored in .env.local on the dev workstation (key: nexcon_api_url). Check that file for the current portal address before attempting browser navigation. Do not display the URL value — confirm it exists with:

(Get-Content "d:\code_2026\p24-infra\.env.local" | Select-String "^nexcon_api_url=") -ne $null
# Returns: True/False

Automation status

Playwright-automatable — Claude can rotate this via the Nexcon customer portal. The portal provides an API management section where keys can be regenerated. Spawn a Playwright agent with the prompt below.

Note: If the Nexcon portal UI has changed or the API key regeneration flow is unclear, fall back to Option B (manual) or contact Nexcon support.


Rotation steps

Option A — Playwright agent (preferred)

Spawn a background agent with this prompt:

You are rotating NEXCON_API_KEY for the Nexcon SIM card provider portal.

IMPORTANT: Never display any secret value in your response. Reference key names only.

1. Read the Nexcon portal URL from .env.local silently:
   $env:NEXCON_URL = (Get-Content "d:\code_2026\p24-infra\.env.local" |
     Select-String "^nexcon_api_url=").ToString().Split('=',2)[1].Trim()
   (Never print $env:NEXCON_URL)

2. Read any existing Nexcon login credentials from .env.local (keys: nexcon_username, nexcon_password)
   Store silently in $env:NEXCON_USER and $env:NEXCON_PASS — never print values.

3. Use Playwright to navigate to $env:NEXCON_URL
   - Log in with the credentials from step 2
   - Navigate to the API management / Developer / API Keys section
   - Find the existing API key — note the key name shown (do not log the value)
   - Click "Regenerate" or "Delete + Create New" as the UI allows
   - Copy the new key value into $env:NEW_KEY (never print it)

4. Update secrets/monitoring.env.sops:
   $env:SOPS_AGE_KEY_FILE = "C:\Users\konar\.age\p24-infra-keys.txt"
   Follow the SOPS write pattern: decrypt → update NEXCON_API_KEY line → re-encrypt → canary.
   Temp file: secrets/monitoring-edit.env.sops
   Use [System.IO.File]::WriteAllText with UTF8Encoding($false).

5. Verify the new key works (replace <NEXCON_API_PATH> with the known API endpoint):
   $headers = @{ "Authorization" = "Bearer $env:NEW_KEY" }
   $r = Invoke-RestMethod "$env:NEXCON_URL/api/v1/sims" -Headers $headers
   Write-Host "SIMs accessible: $($r.Count)"
   $env:NEW_KEY = ""; $env:NEXCON_URL = ""; $env:NEXCON_USER = ""; $env:NEXCON_PASS = ""

6. Commit and open PR targeting main:
   git add secrets/monitoring.env.sops
   git commit -m "chore: rotate NEXCON_API_KEY (scheduled 90d)"
   Push and open PR via gh pr create.

7. After merge: restart nexcon-exporter on vps-i1:
   ssh root@217.154.82.162 "cd /opt/p24-infra/monitoring && docker compose restart nexcon-exporter"

8. Append to docs/secrets-rotation-log.md:
   | <YYYY-MM-DD> | NEXCON_API_KEY | scheduled 90d rotation | AI-agent | SOPS (monitoring) |

Option B — Manual (fallback)

Step 1 — Locate the Nexcon portal URL
  URL: stored in .env.local key nexcon_api_url (never display value)
  Read it: (Get-Content "d:\code_2026\p24-infra\.env.local" | Select-String "^nexcon_api_url=")
  Navigate to that URL in your browser.

Step 2 — Log into Nexcon portal
  Use credentials from .env.local (nexcon_username, nexcon_password)

Step 3 — Regenerate API key
  → Navigate to API / Developer / API Management section
  → Click "Regenerate API Key" or delete existing + create new
  → Copy the new key value immediately

Step 4 — Update secrets/monitoring.env.sops
  $env:SOPS_AGE_KEY_FILE = "C:\Users\konar\.age\p24-infra-keys.txt"
  $plain = sops --decrypt --input-type dotenv --output-type dotenv secrets\monitoring.env.sops
  $tempPath = "$PWD\secrets\monitoring-edit.env.sops"
  $env:NEW_KEY = "<paste-from-step-3>"
  $updated = $plain -replace "^NEXCON_API_KEY=.*", "NEXCON_API_KEY=$env:NEW_KEY"
  [System.IO.File]::WriteAllText($tempPath, ($updated -join "`n") + "`n",
    [System.Text.UTF8Encoding]::new($false))
  $enc = sops --encrypt --input-type dotenv --output-type dotenv $tempPath
  [System.IO.File]::WriteAllText("$PWD\secrets\monitoring.env.sops",
    ($enc -join "`n") + "`n", [System.Text.UTF8Encoding]::new($false))
  # Canary
  sops --decrypt --input-type dotenv --output-type dotenv secrets\monitoring.env.sops | Out-Null
  if ($LASTEXITCODE -ne 0) { throw "SOPS corrupt — do NOT commit" }
  [System.IO.File]::Delete($tempPath)
  $env:NEW_KEY = ""

Step 5 — Commit and push
  git add secrets/monitoring.env.sops
  git commit -m "chore: rotate NEXCON_API_KEY (scheduled 90d)"
  git push
  gh pr create --base main --title "chore: rotate NEXCON_API_KEY"

Step 6 — After merge: restart nexcon-exporter on vps-i1
  ssh root@217.154.82.162 "cd /opt/p24-infra/monitoring && docker compose restart nexcon-exporter"

Step 7 — Append to docs/secrets-rotation-log.md
  | <YYYY-MM-DD> | NEXCON_API_KEY | scheduled 90d rotation | radieu | SOPS (monitoring) |

Verification

# 1. Confirm SOPS updated (key present, expected length 32)
$env:SOPS_AGE_KEY_FILE = "C:\Users\konar\.age\p24-infra-keys.txt"
$line = sops --decrypt --input-type dotenv --output-type dotenv secrets\monitoring.env.sops |
    Select-String "^NEXCON_API_KEY="
$val = $line.ToString().Split('=', 2)[1]
Write-Host "NEXCON_API_KEY length: $($val.Length)"   # expected: 32
 
# 2. Confirm nexcon-exporter is running and returning metrics on vps-i1
ssh root@217.154.82.162 "curl -s http://localhost:9XXX/metrics | head -5"
# Replace 9XXX with the nexcon-exporter port — check monitoring/docker-compose.yml
 
# 3. Check Prometheus scrape health for nexcon-exporter target
# https://prometheus.vps-i1.infra.zintegrowana.online/targets
# Status should be UP

Escalation

SymptomAction
Nexcon portal UI has changedTry navigating to /api, /developer, /settings/api paths; contact Nexcon support if not found
Login credentials rejectedCheck nexcon_username / nexcon_password in .env.local; reset via Nexcon support
API key regeneration not available in UIContact Nexcon support to request key rotation — may need to open a support ticket
nexcon-exporter still returns 401 after rotationCheck secrets-sync.yml has run on vps-i1; force restart: docker compose restart nexcon-exporter
nexcon-exporter port/path unknownCheck monitoring/docker-compose.yml for the nexcon-exporter service definition

Prevention

  • The credential-rotation.yml GH Actions workflow (Monday 06:00 UTC) opens a human-action issue when next_due passes.
  • Nexcon does not send expiry warnings — only the GH Actions check triggers rotation.
  • After rotation, update this playbook’s Last rotated and Next due dates.

Audit Log — Log to infra_operations

After this operation completes, log it to the infra_operations audit table.

Python (Linux server — bms-4, vps-i1, vps-h1, or similar):

import sys
sys.path.insert(0, '/opt/p24-infra')
from scripts.lib.log_op import log_op
 
log_op(
    actor="claude",  # "radieu" for manual human ops, "claude" for agent
    op_type="credential_rotation",
    resource="NEXCON_API_KEY",
    result="success",  # "success" | "failed" | "skipped"
    detail="Scheduled rotation — Nexcon API key regenerated and SOPS monitoring updated",
    env="vps-i1",
    gh_issue=2730,
)

PowerShell (Windows dev machine):

$env:SUPABASE_URL = (Get-Content "C:\code_2026\p24-infra\.env.local" | Select-String "^SUPABASE_URL=").ToString().Split("=",2)[1].Trim()
$env:SUPABASE_SERVICE_KEY = (Get-Content "C:\code_2026\p24-infra\.env.local" | Select-String "^SUPABASE_SERVICE_KEY=").ToString().Split("=",2)[1].Trim()
python -c "
import os, sys
sys.path.insert(0, 'C:/code_2026/p24-infra')
from scripts.lib.log_op import log_op
log_op('claude', 'credential_rotation', 'NEXCON_API_KEY', 'success', 'Scheduled rotation — Nexcon API key regenerated and SOPS monitoring updated', 'vps-i1')
"
$env:SUPABASE_URL = ''; $env:SUPABASE_SERVICE_KEY = ''