wiki(ingest): concepts/verdaccio-token-lifecycle — restart trap + JWT fix

Постмортем утреннего фикса verdaccio: ephemeral secret (нет `secret:` в
конфиге) → инвалидация всех токенов при каждом рестарте; max_users:-1 +
pnpm login → 409; web-UI login как обход; JWT config fix (пинуем secret,
api.jwt 900d). Recipe рефреша для yarn classic vs berry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 16:28:31 +03:00
parent f8ca0794ad
commit 7f998d5222
3 changed files with 116 additions and 0 deletions

View File

@@ -0,0 +1,114 @@
---
title: Verdaccio token lifecycle — restart trap + JWT fix
type: concept
tags: [verdaccio, npm, pnpm, yarn, auth, jwt, gotcha, postmortem]
sources: []
updated: 2026-06-11
---
# Verdaccio token lifecycle — restart trap + JWT fix
## Root cause: auto-generated secret
Verdaccio без явного `secret:` в конфиге генерирует случайный секрет **при каждом запуске**. Все токены (legacy opaque и JWT) подписаны этим секретом — перезапуск контейнера = мгновенная инвалидация всех выданных токенов у всех клиентов.
**Симптом:** `401 Unauthorized` после рестарта VDS/контейнера, несмотря на то что токен в `.npmrc` визуально «есть».
**Фикс (применён 2026-06-11):** явный `secret:` в `config.yaml` → токены переживают рестарты.
## `max_users: -1` + pnpm login → 409
Стандартный workaround «перелогинься» (`pnpm login --registry ...`) не работает при `max_users: -1`.
Verdaccio htpasswd plugin v6 проверяет `max_users` **первым** в `adduser()`, до проверки существования пользователя:
```
if (max_users === -1) → return 409 "user registration disabled"
```
`pnpm login` отправляет `PUT /-/user/org.couchdb.user:<name>` — это registration-path. Verdaccio блокирует даже для существующих юзеров, даже если пароль верный.
Изменение `max_users` на положительное число решает проблему, но открывает регистрацию всем — нежелательно.
## Правильный способ получить токен при `max_users: -1`
Использовать **web-UI login endpoint**, который не проходит через `adduser()`:
```powershell
$body = '{"username":"vitya","password":"..."}'
$r = Invoke-RestMethod -Uri "https://verdaccio.kzntsv.site/-/verdaccio/sec/login" `
-Method POST -Body $body -ContentType "application/json"
$r.token # JWT, 7d expiry
```
Полученный токен — в `~/.npmrc`:
```ini
//verdaccio.kzntsv.site/:_authToken=eyJ...
```
## Конфиг-фикс (2026-06-11)
Добавлено в `/opt/stacks/verdaccio/config/config.yaml` на [vds-kzntsv](../entities/vds-kzntsv.md):
```yaml
secret: <32-byte-hex-permanent>
security:
api:
jwt:
sign:
expiresIn: 30d
notBefore: 0
web:
sign:
expiresIn: 7d
verify: {}
```
До фикса: нет `secret` и нет `security` → legacy tokens, привязанные к ephemeral secret.
После: web-UI и npm-API используют один JWT формат, подписанный постоянным секретом.
> **Примечание:** `security.api.jwt` меняет формат API-токенов с legacy opaque (`5a3X...==`) на JWT (`eyJ...`). Старые legacy-токены в `.npmrc` у всех клиентов перестают работать после смены конфига — нужен refresh.
## Срок жизни токенов
| Endpoint | Expiry | Используется для |
|---|---|---|
| `/-/verdaccio/sec/login` | 7d (web.sign) | Ручной refresh, агенты |
| npm API (pnpm/yarn/npm login) | 30d (api.jwt.sign) | После решения `max_users` проблемы |
## Клиенты: где хранится токен
### npm / pnpm / yarn classic (1.x)
Читают `~/.npmrc` (user-level) или `.npmrc` в корне проекта:
```ini
//verdaccio.kzntsv.site/:_authToken=eyJ...
@snollajs:registry=https://verdaccio.kzntsv.site/
```
Yarn classic читает тот же `~/.npmrc` — update токена в одном файле покрывает всех.
### yarn berry (2.x+)
Не читает `~/.npmrc`. Конфиг в `.yarnrc.yml`:
```yaml
npmRegistries:
"https://verdaccio.kzntsv.site":
npmAuthToken: "eyJ..."
npmAlwaysAuth: true
npmScopes:
snollajs:
npmRegistryServer: "https://verdaccio.kzntsv.site"
```
Refresh токена = обновить `npmAuthToken` в `.yarnrc.yml` каждого проекта, либо использовать `yarn npm login --scope snollajs` (тоже обходит `max_users: -1` через web UI path).
## Связанные страницы
- [verdaccio-prune-semantics](verdaccio-prune-semantics.md) — storage layout и GC
- [vds-kzntsv](../entities/vds-kzntsv.md) — хост где крутится инстанс

View File

@@ -50,6 +50,7 @@ Catalog of all wiki pages. One line per page, organized by type. Updated on ever
- [vbox-windows-stability-tuning](concepts/vbox-windows-stability-tuning.md) — VirtualBox + Windows-гость — нюансы стабильности cross-hypervisor миграции
- [vds-kzntsv-dhcp-outage-2026-05-28](concepts/vds-kzntsv-dhcp-outage-2026-05-28.md) — DHCP outage 2026-05-28 на vds-kzntsv — диагностика + recovery runbook (link UP но без IPv4 → статика + тикет хостеру)
- [verdaccio-prune-semantics](concepts/verdaccio-prune-semantics.md) — Verdaccio prune semantics — proxied vs locally-published
- [verdaccio-token-lifecycle](concepts/verdaccio-token-lifecycle.md) — restart trap (ephemeral secret) + max_users:-1 pnpm 409 + JWT fix + yarn clients
- [winacme-iis-owin-catchall-http01](concepts/winacme-iis-owin-catchall-http01.md) — win-acme HTTP-01 авто-renewal LE на IIS под OWIN-catch-all CMS — challenge в отдельном No-Managed-Code приложении + патч шаблона; заменяет ручной PFX-импорт
- [wd40efax-smr-cascade](concepts/wd40efax-smr-cascade.md) — WD40EFAX SMR Cascade — root cause NAS failure 2026-05-18
- [windows-server-2025-core-bootstrap](concepts/windows-server-2025-core-bootstrap.md) — Win Server 2025 Core (RUVDS) — bootstrap для IIS-хоста: default-blockers + transfer-методов матрица

View File

@@ -2,6 +2,7 @@
Append-only log of wiki operations (ingests, promotions, lints, migrations).
## [2026-06-11] ingest | verdaccio-token-lifecycle — restart trap (ephemeral secret → 401), max_users:-1 + pnpm 409 постмортем, JWT config fix, yarn classic vs berry token refresh recipe.
## [2026-06-11] update | MSSQL backup gap closed — `/opt/stacks/backup/scripts/run.sh` расширен: 5 prod DBs через `docker exec mssql sqlcmd BACKUP DATABASE ... WITH COPY_ONLY, INIT` (Express, без COMPRESSION); .bak в bind-mount → mv в DUMP_DIR → rsync kreknin. Обновлён [[concepts/mssql-on-vds]] (TODO→реализовано).
## [2026-06-11] lint | 10 issues found, 8 fixed inline