Трендовые github проекты в нашем телеграм канале. Подпишись → Когда 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, где важнее память и время старта, набор компромиссов выглядит удачным.