@devix-tecnologia/npm-registry
v0.1.0
Published
Registry npm privado e seguro (Verdaccio) para CapRover: receita one-click genérica, isolamento de rede e as auditorias que a mantêm segura
Readme

@devix-tecnologia/npm-registry
Registry npm privado e endurecido, para qualquer pessoa ou organização subir no CapRover em one-click: Verdaccio sem cadastro aberto, sem acesso anônimo, com versões imutáveis e saída para a internet só por um proxy com allowlist. Nada na receita nem nas imagens é de uma organização: escopo, URL e usuários entram no formulário.
Este pacote traz a receita one-click, o catálogo de variáveis de ambiente
das imagens e as auditorias que mantêm a receita segura e genérica. As
imagens moram no docker/ da raiz do repositório
(docker/verdaccio,
docker/verdaccio/nginx,
docker/proxy-allowlist).
Origem e decisões: TASKS/task-006.
Arquitetura
internet ─► captain-nginx ─► nginx (entrada) ─┐
├─ rede interna ── verdaccio
internet ◄── proxy (saída, allowlist) ◄────────┘| Serviço | Imagem | O que faz |
| --- | --- | --- |
| <app>-verdaccio | devix-verdaccio | O registry. Configuração inteira por env, regerada a cada boot; derruba o boot diante de configuração insegura. |
| <app>-proxy | devix-proxy-allowlist | Squid só com CONNECT 443 para o host do uplink. Nega e registra o resto. |
| <app> | devix-verdaccio-nginx | O único serviço exposto. Proxy reverso; nega as rotas do npm unpublish; limita o corpo; log JSON. |
O que o registry garante
| Garantia | Como | Testado em |
| --- | --- | --- |
| Ninguém se cadastra sozinho | max_users: -1; npm adduser recebe 409 | integração |
| Sem acesso anônimo | nenhuma regra com $all/$anonymous; o entrypoint recusa se aparecer | --check e integração |
| Só quem está em VERDACCIO_PUBLICADORES publica | $authenticated não é aceito como publicador | --check e integração |
| Versão publicada é imutável | republicar dá 409; unpublish dá 403 no nginx e no Verdaccio, para todos | integração |
| Senha só em bcrypt | hash gerado pela imagem; a lista de usuários só aceita hash, com custo mínimo | --check |
| O Verdaccio não sai para a internet por conta própria | rede interna sem rota; a saída é o proxy, com allowlist | integração (compose) e e2e (CapRover 1.15) |
Duas armadilhas do Verdaccio que a imagem contorna
unpublish: falsenão nega nada. No Verdaccio 6,falseou lista vazia emunpublishcontam como "não definido", e a permissão cai na depublish: quem publica, despublica. A imagem põe emunpublishum grupo que nenhum usuário pode ter (ninguem:imutavel— o:é proibido em nome de usuário).- O secret do JWT não vem do
config.yaml. Ele mora no.verdaccio-db.json, dentro do storage. O scriptinit/02-secret.shgrava ali o valor da env a cada boot, então trocarVERDACCIO_SECRETe reiniciar invalida todos os tokens.
Limites que não têm conserto por configuração
npm deprecatetambém é recusado. O Verdaccio trata deprecação como unpublish. Com a imutabilidade, depreciar fica bloqueado — coerente, já que reescreveria os metadados de uma versão publicada. Para avisar quem consome, publique uma versão nova ou mova uma dist-tag (npm dist-tagfunciona).- Publicar um nome que já existe no uplink é aceito. A rota de publish do Verdaccio ignora o 409 da checagem do uplink. Regra operacional: num escopo com proxy, nunca publique aqui um nome que exista lá — a versão local se misturaria à pública. Um teste de caracterização avisa se uma versão futura do Verdaccio passar a recusar.
- O one-click não isola a rede sozinho. Todo app do CapRover nasce na
captain-overlay-network, que tem saída para a internet, e a receita não consegue declarar outra rede. O isolamento é um passo depois do deploy (ver "Isolamento de rede"); até ele, a saída pelo proxy é só configuração.
Subir no CapRover
- Publique as três imagens num registry que o CapRover alcance
(
docker/docker-compose.build.yml, perfilbases). - Em Apps → One-Click Apps/Databases → TEMPLATE, cole
templates/one-click-app.yml. - Preencha pelo menos:
- URL pública (
https://npm.suaempresa.com); - escopos com proxy: só os que a sua organização possui no registry de origem; ou escopos locais, sem proxy;
- quem publica.
As senhas e o secret vêm gerados.
O nome do app não pode ser
registrynemcaptain: são nomes reservados do CapRover, que recusa o deploy.
- URL pública (
- Depois do deploy, obrigatório:
- no app
<app>(o nginx), configure o domínio, habilite HTTPS e marque Force HTTPS — até lá, senha e token trafegam em claro; - aplique o isolamento de rede (próxima seção).
- no app
Isolamento de rede
Depois do one-click, o Verdaccio ainda alcança a internet direto. O isolamento o prende numa rede overlay sem rota para fora; o proxy e o nginx ficam nas duas redes, como fronteiras de saída e de entrada.
# 1. no servidor do CapRover (a API dele não cria rede)
docker network create --driver overlay --internal --attachable npm-interna
# 2. de qualquer máquina que alcance o painel
CAPROVER_SENHA='senha-do-painel' npx @devix-tecnologia/npm-registry isolar \
--url https://captain.suaempresa.com --app npm --rede npm-internaO comando troca o Service Update Override dos três apps, pelo
definirOverrideDoApp do @devix-tecnologia/caprover-client: as redes do
serviço no Swarm, com os aliases srv-captain--<app> pelos quais eles se
acham. É idempotente. Comprovado contra o CapRover 1.15 (pnpm test:e2e):
o override sobrevive a redeploy, e o CapRover não religa a rede padrão
sozinho. Para desfazer, apague o override dos três apps no painel.
Não use o campo de redes do app: no CapRover 1.15 ele é interno, e a rota de atualização não o aceita.
Por que não está na receita. O one-click do CapRover 1.15 não carrega
override de rede: de cada serviço, ele só aproveita image, volumes,
ports, environment e depends_on; de caproverExtra, só
containerHttpPort, notExposeAsWebApp, websocketSupport e
dockerfileLines; e, para o override, só hostname, cap_add e command
(OneClickAppDeploymentHelper e DockerComposeToServiceOverride no código
do servidor). Uma chave networks: ou um serviceUpdateOverride dentro de
caproverExtra é descartado em silêncio, e o app sobe na rede padrão. E,
mesmo que fosse aceito, a rede interna precisa existir antes, e nem a receita
nem a API criam rede. Por isso o isolamento é sempre um passo depois do
one-click.
A senha do usuário inicial fica na env VERDACCIO_ADMIN_SENHA do app
<app>-verdaccio.
Usuários
A env é a fonte de verdade: o htpasswd é regerado a cada boot.
| Para | Faça |
| --- | --- |
| Criar | gere o hash e acrescente nome:hash em VERDACCIO_USUARIOS_HTPASSWD; reinicie |
| Trocar a senha | troque o hash; reinicie |
| Remover | tire o item da lista; reinicie |
htpasswd -nbB maria.souza 'senha-forte-da-maria'Só hash bcrypt entra na lista: senha em claro ali impede o registry de
subir. Nome de usuário não pode ter :, , nem espaço, nem começar com $
ou @. Quem pode criar usuário é quem pode mudar a env no CapRover.
Configurar o cliente
# .npmrc do projeto (versionado)
@suaempresa:registry=https://npm.suaempresa.com/
# token: só por env, nunca no arquivo versionado
//npm.suaempresa.com/:_authToken=${NPM_TOKEN_PRIVADO}O token sai do login (npm login --registry https://npm.suaempresa.com/).
No CI, use um usuário próprio, listado em VERDACCIO_PUBLICADORES.
Pacote privado declara onde publica — sem isso, um npm publish distraído
vai para o npmjs:
{
"publishConfig": {
"registry": "https://npm.suaempresa.com/",
"@suaempresa:registry": "https://npm.suaempresa.com/",
"access": "restricted"
}
}O registry do escopo precisa estar também no publishConfig: um
@suaempresa:registry no .npmrc prevalece sobre o publishConfig.registry
sozinho.
Rotações e incidente
| Situação | Ação |
| --- | --- |
| Token vazou | troque VERDACCIO_SECRET (32 caracteres) e reinicie: todos os tokens caem |
| Senha vazou | troque o hash do usuário (ou VERDACCIO_ADMIN_SENHA) e reinicie; troque também o secret, porque tokens emitidos continuam válidos até expirar |
| Usuário saiu | tire da lista e reinicie; troque o secret pelo mesmo motivo |
| Saída suspeita | o log do proxy lista cada destino (TCP_TUNNEL passou, TCP_DENIED foi barrado) |
Ambiente local
# imagens (da raiz do repositório); as finais fazem FROM da base local
docker build -f docker/verdaccio/Dockerfile.base -t devix-verdaccio-base:6.10.5-1 docker/verdaccio
docker build -t devix-verdaccio:6.10.5-1 docker/verdaccio
docker build -f docker/nginx/Dockerfile.base -t devix-nginx-base:1.27-alpine-1 docker/nginx
docker build -t devix-verdaccio-nginx:1.27-alpine-1 docker/verdaccio/nginx
docker build -f docker/proxy/Dockerfile.base -t devix-proxy-base:6.6-24.04-1 docker/proxy
docker build -t devix-proxy-allowlist:6.6-24.04-1 docker/proxy-allowlist
# segredos (ver secrets/README.md) e subida
cd packages/npm-registry
docker compose up -d --waitVERDACCIO_ESCOPOS, VERDACCIO_PUBLICADORES e as demais variáveis do
docker-compose.yml podem vir de um .env ao lado dele.
Testes
| Comando | O que roda |
| --- | --- |
| pnpm test | catálogo, auditorias da receita e confronto das imagens (ENV dos Dockerfiles, versões, nada de organização embutido) |
| pnpm test:integration | builda as imagens, sobe o compose com rede interna e um uplink falso em HTTPS, e exerce o registry pelo npm: autenticação, autorização, imutabilidade, proxy, isolamento de rede, rotação de secret e de senha. Precisa de Docker. MANTER_AMBIENTE=1 deixa o ambiente de pé no fim |
| pnpm test:e2e | sobe um CapRover 1.15 de verdade num Docker isolado (dind), instala a receita pelo endpoint de one-click, roda o npm-registry isolar e confere o antes e o depois: saída direta, saída pelo proxy, acesso pelo domínio, unpublish negado e o isolamento depois de um redeploy. Mesmo arranjo do e2e do geohub. Precisa de Docker com container privilegiado e de internet; rode pnpm build antes |
API
import { criarCapRoverClient } from '@devix-tecnologia/caprover-client';
import {
auditarReceitaDoRegistry,
isolarNoCapRover,
lerTextoDaReceita,
} from '@devix-tecnologia/npm-registry';
auditarReceitaDoRegistry(lerTextoDaReceita()); // [] = receita aprovada
// O isolamento, como biblioteca (o `npm-registry isolar` usa isto), sobre o
// cliente do CapRover do devix-stack.
const cliente = await criarCapRoverClient({
appName: 'npm',
senha: process.env.CAPROVER_SENHA!,
url: 'https://captain.suaempresa.com',
});
await isolarNoCapRover({ cliente, appName: 'npm', rede: 'npm-interna' });auditarReceitaDoRegistry roda as regras genéricas do
@devix-tecnologia/caprover-stack e as deste pacote:
| Regra | Reprova |
| --- | --- |
| paridade-com-catalogo | env fora do catálogo, valor que não bate com a origem, default diferente do da imagem, variável que não alimenta nada |
| segredo-gerado | credencial com default fixo em vez de $$cap_gen_random_* |
| superficie-desligada-por-padrao | interface web ou npm audit ligados por padrão |
| servicos-internos-nao-expostos | Verdaccio ou proxy expostos como web app |
| sem-colisao-de-prefixo | id de variável que é prefixo de outro (a substituição do CapRover corromperia o longo) |
| sem-valor-de-organizacao | valor de organização embutido em env ou default |
Pendências
- Onde publicar as imagens: os defaults da receita apontam para
registry.exemplo.com, como o resto da stack. Para servir a qualquer organização, as imagens precisam estar num registry público.
