Playbook: IONOS_API_TOKEN Rotation
Service: IONOS Developer Portal (https://developer.hosting.ionos.de)
Secrets: IONOS_API_TOKEN_PREFIX + IONOS_API_TOKEN_ENCRYPTION
Rotation frequency: 90 days (or immediately on suspected exposure)
Last rotated: 2026-06-27
Next due: 2026-09-27
Token structure
The IONOS API token is split into two parts:
| Part | Key name | Length | Purpose |
|---|---|---|---|
| Public prefix | IONOS_API_TOKEN_PREFIX | 32 chars | Identifies the token (not secret alone) |
| Encryption part | IONOS_API_TOKEN_ENCRYPTION | 86 chars | Secret component |
| Combined | Used in requests as | PREFIX.ENCRYPTION | Value passed in X-API-Key header |
Rotate both parts together — they are a single credential generated together in the IONOS portal.
What uses this credential
| Consumer | How | Effect if missing |
|---|---|---|
cost-exporter container (vps-i1) | X-API-Key: PREFIX.ENCRYPTION header to IONOS billing API | VPS cost metrics missing from Grafana |
scripts/ionos-*.py / dns-manager.py | IONOS API calls for DNS management | DNS operations fail |
| GH Actions provisioning workflow | VPS provisioning via IONOS Cloud API | New VPS provisioning fails |
Where stored
| Location | Key names | How to update |
|---|---|---|
secrets/monitoring.env.sops | IONOS_API_TOKEN_PREFIX, IONOS_API_TOKEN_ENCRYPTION | SOPS write pattern (see below) |
.env.local on dev workstation | IONOS_API_TOKEN_PREFIX, IONOS_API_TOKEN_ENCRYPTION | Edit both lines |
| vps-i1 deployed env | /opt/p24-infra/monitoring/.env | Auto-synced by secrets-sync.yml on merge to dev/main |
GH Secret radieu/p24-infra | Check docs/infrastructure-overview.md | gh secret set |
Also see: docs/playbooks/ionos-api-server-management.md for full IONOS operations reference.
Automation status
Playwright-automatable — Claude can rotate this via the IONOS Developer Portal. The portal allows creating and deleting API tokens under “API Keys” or “Developer Portal”. Spawn a Playwright agent with the prompt below.
Rotation steps
Option A — Playwright agent (preferred)
Spawn a background agent with this prompt:
You are rotating IONOS_API_TOKEN_PREFIX and IONOS_API_TOKEN_ENCRYPTION for the IONOS hosting account.
IMPORTANT: Never display any secret value in your response. Reference key names only.
Both parts must be rotated together — they are generated as a pair.
1. Use Playwright to navigate to https://developer.hosting.ionos.de
- Log in with radieu@gmail.com credentials
(IONOS login may use the associated email for the IONOS account — check .env.local for
IONOS_ACCOUNT_EMAIL if the login email differs from radieu@gmail.com)
- Navigate to "API Keys" or "Manage API Tokens" section
- Find the existing token (likely named "p24-infra" or similar)
- Note its label — do NOT log the prefix or encryption values
- Click "Delete" or "Revoke" on the old token → Confirm
2. Create a new token:
- Click "Create API Key" or "Generate new token"
- Label: p24-infra-<YYYY-MM-DD>
- The portal will display the token as: PREFIX.ENCRYPTION (split by a dot)
- Split on the first dot: PREFIX = part before first dot, ENCRYPTION = part after first dot
- Store silently:
$env:NEW_PREFIX = <32-char part before dot>
$env:NEW_ENC = <86-char part after dot>
- Never print either value
3. Verify lengths before writing:
Write-Host "PREFIX length: $($env:NEW_PREFIX.Length)" # expected: 32
Write-Host "ENC length: $($env:NEW_ENC.Length)" # expected: 86
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"
$updated = $plain -replace "^IONOS_API_TOKEN_PREFIX=.*", "IONOS_API_TOKEN_PREFIX=$env:NEW_PREFIX"
$updated = $updated -replace "^IONOS_API_TOKEN_ENCRYPTION=.*", "IONOS_API_TOKEN_ENCRYPTION=$env:NEW_ENC"
[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)
5. Verify the new token works:
$combined = "$env:NEW_PREFIX.$env:NEW_ENC"
$r = Invoke-RestMethod "https://api.hosting.ionos.com/dns/v1/zones" `
-Headers @{ "X-API-Key" = $combined }
Write-Host "DNS zones accessible: $($r.Count)"
$combined = ""; $env:NEW_PREFIX = ""; $env:NEW_ENC = ""
6. Commit and open PR targeting main:
git add secrets/monitoring.env.sops
git commit -m "chore: rotate IONOS_API_TOKEN (scheduled 90d)"
Push and open PR via gh pr create.
7. After merge: restart cost-exporter on vps-i1:
ssh root@217.154.82.162 "cd /opt/p24-infra/monitoring && docker compose restart cost-exporter"
8. Append to docs/secrets-rotation-log.md:
| <YYYY-MM-DD> | IONOS_API_TOKEN_PREFIX + ENCRYPTION | scheduled 90d rotation | AI-agent | SOPS (monitoring) |
Option B — Manual (fallback)
Step 1 — Log into IONOS Developer Portal
URL: https://developer.hosting.ionos.de
Use radieu@gmail.com (or the IONOS account email — check .env.local for IONOS_ACCOUNT_EMAIL)
Step 2 — Delete old token
→ API Keys (or similar section)
→ Find "p24-infra" or current active token
→ Delete / Revoke → Confirm
Step 3 — Create new token
→ Create API Key / Generate Token
→ Label: p24-infra-<YYYY-MM-DD>
→ The portal shows: PREFIX.ENCRYPTION (one string with a dot separator)
→ Split on the first dot:
- PREFIX: 32 chars before the first dot
- ENCRYPTION: 86 chars after the first dot
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_PREFIX = "<32-char prefix from step 3>"
$env:NEW_ENC = "<86-char encryption from step 3>"
$updated = $plain -replace "^IONOS_API_TOKEN_PREFIX=.*", "IONOS_API_TOKEN_PREFIX=$env:NEW_PREFIX"
$updated = $updated -replace "^IONOS_API_TOKEN_ENCRYPTION=.*",
"IONOS_API_TOKEN_ENCRYPTION=$env:NEW_ENC"
[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_PREFIX = ""; $env:NEW_ENC = ""
Step 5 — Commit and push
git add secrets/monitoring.env.sops
git commit -m "chore: rotate IONOS_API_TOKEN (scheduled 90d)"
git push
gh pr create --base main --title "chore: rotate IONOS_API_TOKEN"
Step 6 — After merge: restart cost-exporter
ssh root@217.154.82.162 "cd /opt/p24-infra/monitoring && docker compose restart cost-exporter"
Step 7 — Append to docs/secrets-rotation-log.md
| <YYYY-MM-DD> | IONOS_API_TOKEN_PREFIX + ENCRYPTION | scheduled 90d rotation | radieu | SOPS (monitoring) |
Verification
# 1. Confirm SOPS updated (both keys present with expected lengths)
$env:SOPS_AGE_KEY_FILE = "C:\Users\konar\.age\p24-infra-keys.txt"
$decrypted = sops --decrypt --input-type dotenv --output-type dotenv secrets\monitoring.env.sops
$prefix = ($decrypted | Select-String "^IONOS_API_TOKEN_PREFIX=").ToString().Split('=',2)[1]
$enc = ($decrypted | Select-String "^IONOS_API_TOKEN_ENCRYPTION=").ToString().Split('=',2)[1]
Write-Host "PREFIX length: $($prefix.Length)" # expected: 32
Write-Host "ENC length: $($enc.Length)" # expected: 86
# 2. Test API call (use combined token — never print it)
$combined = "$prefix.$enc"
$r = Invoke-RestMethod "https://api.hosting.ionos.com/dns/v1/zones" `
-Headers @{ "X-API-Key" = $combined }
Write-Host "DNS zones count: $($r.Count)" # expected: >0
$combined = ""; $prefix = ""; $enc = ""; $env:SOPS_AGE_KEY_FILE = ""
# 3. Check cost-exporter metrics after container restart
ssh root@217.154.82.162 "curl -s http://localhost:9210/metrics | grep ionos | head -5"Escalation
| Symptom | Action |
|---|---|
| IONOS portal URL has changed | Try https://my.ionos.de → Developer API section |
| Login fails | IONOS login uses a separate IONOS account email; check .env.local for IONOS_ACCOUNT_EMAIL |
| Token shown without dot separator | Some portal versions show PREFIX and ENCRYPTION in separate fields; combine manually |
| cost-exporter still shows old metrics | Container needs restart; check secrets-sync.yml has merged and run |
| DNS operations fail after rotation | scripts/dns-manager.py reads from .env on vps-i1; wait for sync or manually update /opt/p24-infra/monitoring/.env |
Prevention
- The
credential-rotation.ymlGH Actions workflow (Monday 06:00 UTC) opens ahuman-actionissue whennext_duepasses. - IONOS does not send expiry warnings for API tokens.
- Always rotate both PREFIX and ENCRYPTION together — they are a matched pair from a single generation event.
- After rotation, update this playbook’s Last rotated and Next due dates.
- See also:
docs/playbooks/ionos-api-server-management.mdfor related IONOS API operations.
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="IONOS_API_TOKEN",
result="success", # "success" | "failed" | "skipped"
detail="Scheduled rotation — IONOS API token 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', 'IONOS_API_TOKEN', 'success', 'Scheduled rotation — IONOS API token regenerated and SOPS monitoring updated', 'vps-i1')
"
$env:SUPABASE_URL = ''; $env:SUPABASE_SERVICE_KEY = ''