Logo Craft Homelab Docs Контакты Telegram
NORA: лёгкий artifact registry на Rust с карантином пакетов и блокировкой CVE Трендовые github проекты в нашем телеграм канале. Подпишись →
27 августа 2026 г.

Когда Nexus и Artifactory кажутся слишком тяжёлыми для домашней инфраструктуры

Проблема хранения артефактов знакома любому, кто собирает сервисы: Docker-образы, npm-пакеты, Python wheels, Maven-артефакты, крейты Rust. Дальше обычно два пути. Первый — тянуть всё напрямую из публичных реестров (Docker Hub, npmjs.org, PyPI), где регулярно упираешься в rate limit и региональные блокировки. Второй — поднять свой Nexus, Artifactory или Harbor. Это Java-платформа с JVM, отдельной СУБД (PostgreSQL или OrientDB), 2–4 ГБ RAM в простое и десятками секунд на холодный старт. Для домашней стойки или небольшого кластера это заметная нагрузка на бюджет по памяти.

NORA закрывает середину между этими вариантами. Это open-source реестр артефактов на Rust с лицензией MIT: один бинарник меньше 27 МБ, потребление около 50 МБ RAM в простое, старт за три секунды. Внешних зависимостей нет — ни Java, ни базы данных. Метаданные лежат на файловой системе, а сами артефакты можно сразу писать в S3, чего бесплатные редакции Nexus и Artifactory не умеют.

Что внутри

Ядро построено на Tokio и Axum. Метаданные хранятся локально, бинарные данные — на локальном диске или в S3-совместимом объектном хранилище. Поды при этом остаются stateless: состояние живёт в Object Storage, а сам под можно пересоздавать без потери данных. Для домашней или небольшой продовой инфраструктуры это означает простой сценарий бэкапа — достаточно копировать бакет и включить на нём версионирование.

Поддерживается 15 форматов: Docker/OCI, Helm через OCI, npm, PyPI, Maven, Cargo, Go, Raw, RubyGems, Terraform, Ansible Galaxy, NuGet, Pub (Dart/Flutter), Conan (C/C++), RPM и Debian/APT. По количеству форматов NORA уступает Nexus и Artifactory, но покрывает основные экосистемы, которые реально встречаются в сборках.

Публикация и проксирование

Важный нюанс: не для всех форматов реализована загрузка. Часть реестров работает только как pull-through кэш — зеркало апстрима с сохранением скачанных пакетов. Полноценный push/publish сейчас есть у Docker/OCI, Helm OCI, npm, PyPI, Maven, а также у hosted-форматов RPM, Debian/APT и Raw. Cargo, Go, Terraform, RubyGems, NuGet, Ansible Galaxy, Pub и Conan доступны в режиме проксирования и кэширования: клиенты вроде bundle install, dotnet restore, go get и conan install через NORA работают, а команды публикации для этих форматов пока не поддержаны протоколом или не реализованы.

Из 15 форматов только Raw не имеет апстрима — это хранилище произвольных файлов по произвольным путям, удобное для релизных артефактов и бинарников. Остальные 14 умеют кэшировать апстрим. Docker, npm, PyPI, Maven, Cargo, Go, Terraform, RubyGems, NuGet, Ansible, Pub и Conan проксируют из коробки, а RPM и Debian/APT по умолчанию работают как hosted-репозитории и включают проксирование только явно, через маппинг имени репозитория на апстрим в конфиге.

Как устроен кэш

Кэш делится на два типа. Иммутабельный контент — конкретные версии пакетов, tarball’ы, .crate, .info/.mod/.zip Go-модулей, blob’ы Docker — сохраняется навсегда после первого скачивания по принципу content-addressed хранения. Это основная защита от rate limit и недоступности апстрима. Метаданные — индексы, списки версий, packument’ы npm, repodata — кэшируются с TTL (по умолчанию 300 секунд, настраивается per-registry). По истечении TTL NORA перепроверяет метаданные у апстрима через If-None-Match и If-Modified-Since, а если апстрим недоступен — отдаёт устаревшую копию вместо ошибки.

Начиная с версии 1.2.0 все форматы поддерживают докачку прерванных загрузок через HTTP Range и 206 Partial Content: curl -C -, pip, apt продолжат передачу с места обрыва. Мутабельный контент из докачки исключён, чтобы не склеить два поколения одного индекса.

Развёртывание

Стандартный путь — Helm-чарт. Добавляем репозиторий, генерируем values и ставим:

helm repo add nora https://getnora-io.github.io/helm-charts
helm repo update
helm upgrade --install nora nora/nora --version 0.4.4 -f helm-values.yaml

В values указываем ingress с TLS через cert-manager, отключаем локальный диск и переключаем storage в режим S3:

persistence:
  enabled: false
config:
  server:
    public_url: "https://registry.example.com"
  storage:
    mode: s3
    s3_url: https://storage.yandexcloud.net
    bucket: nora-storage
    s3_region: ru-central1
  registries:
    enable: "all"

Ресурсы можно ограничить скромно: requests 128 Mi памяти и 0.25 CPU, limits 512 Mi и 1 CPU. Для air-gapped-сред заранее понадобится прокси для скачивания пакетов из реестров с гео-ограничениями (например, Terraform Registry).

По умолчанию NORA отдаёт анонимное чтение. Аутентификация включается через htpasswd-файл с bcrypt, положенный в Kubernetes Secret. Дальше работают API-токены с префиксом nra_ и тремя ролями: read, write, admin. Токен создаётся POST-запросом на /api/tokens с указанием роли и TTL в днях и используется как пароль в docker login, .npmrc, GOPROXY, ~/.netrc и других клиентах.

Защита цепочки поставок

Здесь у NORA два механизма, которых обычно нет в лёгких реестрах.

Min Release Age

Одна строка в конфиге блокирует пакеты, опубликованные меньше N дней назад. Большинство вредоносных пакетов выявляют за 1–3 дня, поэтому недельный буфер отсекает свежие компрометации. Аналог есть в .npmrc (min-release-age) и uv.toml (exclude-newer), но NORA применяет правило ко всем 15 форматам и поддерживает переопределение на уровне отдельного реестра:

config:
  curation:
    mode: "enforce"
    min_release_age: "7d"
    npm:
      min_release_age: "3d"
    pypi:
      min_release_age: "5d"

Дата публикации берётся из реальных метаданных апстрима: time у npm, upload-time у PyPI, аналогичные поля у Cargo, Go, NuGet, Conan, pub.dev, Maven Central, RubyGems, Ansible Galaxy, Terraform. Для форматов без дат публикации (Docker/OCI) NORA фиксирует момент первого появления каждого digest по своим часам и держит новые digest в карантине до истечения порога — подменить это апстрим не может. Если дата неизвестна, а карантин активен, пакет блокируется (fail-closed). На экстренный случай есть заголовок X-Nora-Bypass-Token со сравнением в constant-time.

CVE Blocking

Это управляемый blocklist известных уязвимых версий, а не сканер вроде Trivy или Snyk. Список заполняется вручную или экспортом из баз уязвимостей:

{
  "version": 1,
  "rules": [
    { "registry": "npm", "name": "event-stream", "version": "3.3.6",
      "reason": "malicious flatmap-stream dependency" },
    { "registry": "*", "name": "log4j*", "version": "2.*",
      "reason": "Log4Shell RCE" }
  ]
}

Правила понимают glob-паттерны, включая foo.* для groupId Maven и foo/** для Go-модулей, и работают во всех форматах. Blocklist монтируется через existingConfigMap — чарт сам создаёт volume и прописывает путь в конфиг. Рядом можно включить allowlist в режиме default-deny, где проходят только явно перечисленные тройки (реестр, имя, версия) с опциональным пиннингом по SHA-256. Режим audit логирует совпадения без блокировки — полезно для прогона перед включением в production.

Дополнительно всегда работает изоляция пространств имён: внутренние имена пакетов никогда не уходят в апстрим, что закрывает dependency confusion даже при выключенной курации. Контрольные суммы SHA-256/SHA-512 проверяются при каждой отдаче.

Air-gapped и наблюдаемость

Для изолированных сред есть встроенная утилита nora mirror, которая предварительно наполняет кэш по lockfile:

nora mirror npm --lockfile package-lock.json --registry https://registry.example.com
nora mirror pip --requirement requirements.txt --registry https://registry.example.com
nora mirror docker --images "nginx:latest,redis:7,node:20-alpine" --registry https://registry.example.com

После зеркалирования реестр работает автономно, без обращений наружу. Метрики отдаются в формате Prometheus по /metrics: количество запросов по реестрам, latency, объём хранилища, число артефактов. Есть /health, /ready, Web UI на /ui/ и Swagger на /api-docs.

Кому подойдёт

NORA имеет смысл там, где нужен быстрый приватный кэш перед публичными реестрами и лёгкое hosted-хранилище для своих Docker-образов, Helm-чартов и npm/PyPI/Maven-пакетов, а разворачивать ради этого Java-платформу с отдельной СУБД не хочется. Ограничения тоже понятные: меньше форматов с полноценной публикацией, отсутствие SQLite-метаданных и delta-RPM, генерация только XML-repodata, часть форматов — исключительно на чтение. Для домашней стойки, небольшого кластера k3s или CI, где важнее память и время старта, набор компромиссов выглядит удачным.