Self-hosted платформа для полного управления Nebula mesh-сетью. Два основных компонента: management server (nebula-mgmt) и agent (nebula-agent). Go монорепо, single module.
nebula-mgmt/
├── go.mod # github.com/forgekeep/nebula-mesh
├── go.sum
├── .gitignore
├── .golangci.yml # Linter configuration
├── Makefile # Build targets
├── README.md
├── nebula-research.md # Результаты исследования
│
├── cmd/
│ ├── nebula-mgmt/ # Management server binary
│ │ └── main.go
│ └── nebula-agent/ # Agent binary
│ └── main.go
│
├── internal/
│ ├── config/ # Конфигурация server и agent
│ │ ├── server.go # ServerConfig struct + parse
│ │ ├── server_test.go
│ │ ├── agent.go # AgentConfig struct + parse
│ │ └── agent_test.go
│ │
│ ├── pki/ # PKI operations (обёртка над nebula/cert)
│ │ ├── ca.go # CA creation, load, save
│ │ ├── ca_test.go
│ │ ├─�� signer.go # Certificate signing
│ │ ├── signer_test.go
│ │ ├── blocklist.go # Blocklist management
│ │ └── blocklist_test.go
│ │
│ ��── store/ # Database layer
│ │ ├── store.go # Store interface
│ │ ├── sqlite.go # SQLite implementation
│ │ ├── sqlite_test.go
│ │ ├── hosts.go # Host CRUD
│ │ ├── hosts_test.go
│ │ ├── tokens.go # Enrollment tokens
│ │ ├── tokens_test.go
│ │ ├── networks.go # Network settings
│ │ ├── networks_test.go
│ │ └── migrations/
│ │ ├── embed.go # go:embed migrations
│ │ ├── 001_initial.up.sql
│ │ └── 001_initial.down.sql
│ │
│ ├── configgen/ # Nebula config.yml generator
│ │ ├── generator.go # Template-based config generation
│ │ ├── generator_test.go
│ │ └── templates/
│ │ ├── lighthouse.yml.tmpl
│ │ ├── host.yml.tmpl
│ │ └── relay.yml.tmpl
│ │
│ ├── api/ # REST API (server)
│ │ ├── server.go # HTTP server setup, chi router
│ │ ├── middleware.go # Auth, logging, recovery
│ │ ├── middleware_test.go
│ │ ├── hosts.go # Host management endpoints
│ │ ├── hosts_test.go
�� │ ├── enroll.go # Enrollment endpoint
│ │ ├── enroll_test.go
│ │ ├── updates.go # Agent poll endpoint
│ │ ├── updates_test.go
│ │ ├── networks.go # Network management endpoints
│ │ └── networks_test.go
│ │
│ ├── agent/ # Agent logic
│ │ ├���─ enroll.go # Enrollment flow
│ │ ├── enroll_test.go
│ │ ├── poller.go # Poll loop + apply updates
│ │ ├── poller_test.go
│ │ ├── nebula.go # Nebula process management (SIGHUP, status)
│ │ └���─ nebula_test.go
│ │
│ ├── cli/ # CLI commands (server)
│ │ ├── init.go # nebula-mgmt init
│ │ ├── serve.go # nebula-mgmt serve
│ │ ├── host.go # nebula-mgmt host create/list/delete
│ │ └── network.go # nebula-mgmt network create/list
│ │
│ └── models/ # Shared domain models
│ ├── host.go # Host, HostStatus
│ ├─��� network.go # Network, CIDR settings
│ ├── token.go # EnrollmentToken
│ └── certificate.go # CertificateInfo (metadata, not key material)
│
├── tests/
│ └── integration/
│ └── e2e_test.go # End-to-end: server + agent
│
└── configs/
├── server.example.yml # Example server config
└── agent.example.yml # Example agent config
github.com/slackhq/nebula # PKI library (cert package)
github.com/go-chi/chi/v5 # HTTP router
modernc.org/sqlite # Pure Go SQLite driver
gopkg.in/yaml.v3 # YAML parsing (Nebula config compat)
github.com/google/uuid # Token generation
Без CGO. Pure Go для кросс-компиляции (Linux, macOS, Windows).
-- 001_initial.up.sql
CREATE TABLE networks (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
cidr TEXT NOT NULL, -- e.g., "192.168.100.0/24"
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE hosts (
id TEXT PRIMARY KEY,
network_id TEXT NOT NULL REFERENCES networks(id),
name TEXT NOT NULL,
nebula_ip TEXT NOT NULL, -- e.g., "192.168.100.5"
groups TEXT NOT NULL DEFAULT '[]', -- JSON array
role TEXT NOT NULL DEFAULT 'host', -- host, lighthouse, relay
is_lighthouse BOOLEAN NOT NULL DEFAULT 0,
is_relay BOOLEAN NOT NULL DEFAULT 0,
public_ip TEXT, -- for lighthouse/relay
listen_port INTEGER DEFAULT 4242, -- for lighthouse/relay
status TEXT NOT NULL DEFAULT 'pending', -- pending, enrolled, blocked
cert_fingerprint TEXT, -- current cert fingerprint
cert_expires_at DATETIME,
last_seen_at DATETIME,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(network_id, name),
UNIQUE(network_id, nebula_ip)
);
CREATE TABLE enrollment_tokens (
id TEXT PRIMARY KEY,
host_id TEXT NOT NULL REFERENCES hosts(id) ON DELETE CASCADE,
token TEXT NOT NULL UNIQUE,
used BOOLEAN NOT NULL DEFAULT 0,
expires_at DATETIME NOT NULL,
used_at DATETIME,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE certificates (
id TEXT PRIMARY KEY,
host_id TEXT NOT NULL REFERENCES hosts(id) ON DELETE CASCADE,
fingerprint TEXT NOT NULL UNIQUE,
pem TEXT NOT NULL, -- PEM-encoded certificate (no private key)
not_before DATETIME NOT NULL,
not_after DATETIME NOT NULL,
is_current BOOLEAN NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE blocklist (
fingerprint TEXT PRIMARY KEY,
host_id TEXT REFERENCES hosts(id),
reason TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE network_config (
network_id TEXT NOT NULL REFERENCES networks(id),
key TEXT NOT NULL, -- e.g., "firewall.inbound", "lighthouse.dns"
value TEXT NOT NULL, -- JSON value
PRIMARY KEY (network_id, key)
);POST /api/v1/networks # Создать сеть
GET /api/v1/networks # Список сетей
GET /api/v1/networks/{id} # Получить сеть
POST /api/v1/hosts # Создать хост → возвращает enrollment token
GET /api/v1/hosts # Список хостов (фильтр по network, group, status)
GET /api/v1/hosts/{id} # Получить хост
DELETE /api/v1/hosts/{id} # Удалить хост (добавить cert в blocklist)
POST /api/v1/hosts/{id}/block # Заблокировать хост
POST /api/v1/hosts/{id}/renew # Принудительное обновление сертификата
GET /api/v1/blocklist # Текущий blocklist
GET /health # Health check
POST /api/v1/enroll # Enrollment: token + public key → cert + config
GET /api/v1/agent/updates?fingerprint=X # Poll: получить обновления
POST /api/v1/agent/status # Report: статус агента (optional)
// POST /api/v1/enroll
// Request:
{
"token": "tok_abc123...",
"public_key_pem": "-----BEGIN NEBULA X25519 PUBLIC KEY-----\n..."
}
// Response (200 OK):
{
"certificate_pem": "-----BEGIN NEBULA CERTIFICATE-----\n...",
"ca_certificate_pem": "-----BEGIN NEBULA CERTIFICATE-----\n...",
"config_yaml": "pki:\n ca: /etc/nebula/ca.crt\n..."
}// GET /api/v1/agent/updates?fingerprint=abc123
// Response (200 OK, есть обновления):
{
"has_updates": true,
"certificate_pem": "-----BEGIN NEBULA CERTIFICATE-----\n...", // null if not changed
"ca_certificate_pem": "-----BEGIN NEBULA CERTIFICATE-----\n...", // null if not changed
"config_yaml": "...", // null if not changed
"blocklist": ["fingerprint1", "fingerprint2"] // full current blocklist
}
// Response (304 Not Modified, нет обновлений):
// Empty bodyЦель: рабочий сервер + агент, позволяющие создать сеть, добавить хосты через enrollment, и управлять ими через CLI + API.
Что делаем:
go mod init github.com/forgekeep/nebula-mesh- Минимальные
main.goдля обоих бинарников (пустые, но компилируемые) .gitignore(бинарники, SQLite files,*.key,.env).golangci.yml(golangci-lint config)Makefileс target'ами:build,test,lint,all
Критерий готовности: make all проходит без ошибок.
Server config (configs/server.example.yml):
listen: ":8080"
data_dir: "/var/lib/nebula-mgmt"
db_path: "/var/lib/nebula-mgmt/nebula.db"
api_key: "" # generated on init
log_level: "info"Agent config (configs/agent.example.yml):
server_url: "https://mgmt.example.com:8080"
data_dir: "/etc/nebula"
poll_interval: "30s"
nebula_config_path: "/etc/nebula/config.yml"
nebula_pid_file: "/run/nebula.pid"Реализация:
internal/config/server.go—ServerConfigstruct,LoadServerConfig(path) (*ServerConfig, error)internal/config/agent.go—AgentConfigstruct,LoadAgentConfig(path) (*AgentConfig, error)- Defaults для всех полей, валидация при загрузке
Обёртка над nebula/cert с persistence.
internal/pki/ca.go:
type CAManager struct {
caCert cert.Certificate
caKey []byte // decrypted private key in memory
}
func NewCA(name string, duration time.Duration) (*CAManager, error)
// Returns: manager
func LoadCAFromMaterial(certPEM []byte, rawKey ed25519.PrivateKey) (*CAManager, error)
func (m *CAManager) CACertPEM() []byte
func (m *CAManager) CACertFingerprint() stringinternal/pki/signer.go:
type SignRequest struct {
Name string
PublicKey []byte
Networks []netip.Prefix
Groups []string
Duration time.Duration
}
func (m *CAManager) Sign(req SignRequest) (cert.Certificate, error)internal/pki/blocklist.go:
type Blocklist struct {
mu sync.RWMutex
fingerprints map[string]struct{}
}
func NewBlocklist() *Blocklist
func (b *Blocklist) Add(fingerprint string)
func (b *Blocklist) Remove(fingerprint string)
func (b *Blocklist) Contains(fingerprint string) bool
func (b *Blocklist) List() []stringinternal/store/store.go — interface:
type Store interface {
// Networks
CreateNetwork(ctx context.Context, n *models.Network) error
GetNetwork(ctx context.Context, id string) (*models.Network, error)
ListNetworks(ctx context.Context) ([]*models.Network, error)
// Hosts
CreateHost(ctx context.Context, h *models.Host) error
GetHost(ctx context.Context, id string) (*models.Host, error)
GetHostByFingerprint(ctx context.Context, fingerprint string) (*models.Host, error)
ListHosts(ctx context.Context, filter HostFilter) ([]*models.Host, error)
UpdateHost(ctx context.Context, h *models.Host) error
DeleteHost(ctx context.Context, id string) error
// Enrollment tokens
CreateToken(ctx context.Context, t *models.EnrollmentToken) error
ConsumeToken(ctx context.Context, token string) (*models.EnrollmentToken, error)
// ConsumeToken: returns token if valid and not used, marks as used atomically
// Certificates
SaveCertificate(ctx context.Context, hostID string, certPEM []byte, fp string, notBefore, notAfter time.Time) error
GetCurrentCertificate(ctx context.Context, hostID string) ([]byte, error)
// Blocklist
AddToBlocklist(ctx context.Context, fingerprint, hostID, reason string) error
RemoveFromBlocklist(ctx context.Context, fingerprint string) error
GetBlocklist(ctx context.Context) ([]string, error)
// Migrations
Migrate(ctx context.Context) error
Close() error
}internal/store/sqlite.go — SQLite реализация через modernc.org/sqlite + database/sql.
Миграции embedded через go:embed.
internal/configgen/generator.go:
type GeneratorInput struct {
Host *models.Host
Network *models.Network
CACertPath string
CertPath string
KeyPath string
Lighthouses []LighthouseInfo // IP → public address mapping
FirewallRules []FirewallRule
}
type LighthouseInfo struct {
NebulaIP string
PublicAddr string // "1.2.3.4:4242"
}
func Generate(input GeneratorInput) ([]byte, error) // returns YAMLGo text/template с YAML output. Шаблоны embedded через go:embed.
Chi router с middleware chain:
RequestID— уникальный ID запросаLogger— structured logging черезslogRecoverer— panic recoveryBearerAuth— проверка API key (только для management endpoints)
Enrollment endpoint (POST /api/v1/enroll):
- Parse token из request body
store.ConsumeToken()— валидация + инвалидация в одной транзакции- Parse public key PEM из request body
pki.Sign()— подписание сертификатаconfiggen.Generate()— генерация config.ymlstore.SaveCertificate()— сохранение cert metadatastore.UpdateHost()— status = "enrolled"- Return: cert PEM + CA cert PEM + config YAML
Agent updates endpoint (GET /api/v1/agent/updates?fingerprint=X):
store.GetHostByFingerprint()— найти хост- Проверить: cert renewed? config changed? blocklist changed?
- Если нет изменений → 304
- Если есть → 200 с обновлёнными данными
Enrollment flow (internal/agent/enroll.go):
- Генерировать X25519 keypair (
crypto/rand) POST /api/v1/enrollс token + public key PEM- Сохранить:
ca.crt,host.crt,host.key,config.ymlв data_dir - Permissions:
0600для ключей,0644для сертификатов
Poll loop (internal/agent/poller.go):
- Каждые N секунд:
GET /api/v1/agent/updates?fingerprint=<current> - При 304 → sleep
- При 200 → обновить файлы, отправить SIGHUP Nebula (через PID file или
pkill -HUP nebula) - При ошибке → логировать, retry с backoff
CLI:
nebula-agent --server=<URL> --token=<TOKEN> [--config=<path>] [--data-dir=<dir>]— первый запуск: enroll, записьagent.yml(0600), старт поллераnebula-agent [--config=<path>]— последующие запуски (token не нужен)nebula-agent --update-config --server=<URL>— атомарно переписать поле вagent.yml- Legacy (deprecated, поддерживается один релиз):
nebula-agent enroll …/nebula-agent run …
nebula-mgmt init [--config=<path>]— создать CA, сгенерировать API key, инициализировать DBnebula-mgmt serve [--config=<path>]— запустить HTTP servernebula-mgmt host create --name=<name> --ip=<nebula-ip> --network=<id> [--groups=<g1,g2>] [--lighthouse] [--public-ip=<ip>]nebula-mgmt host list [--network=<id>]nebula-mgmt host delete --id=<id>nebula-mgmt network create --name=<name> --cidr=<cidr>nebula-mgmt network list
E2E test без внешних зависимостей:
init— создать CA + DB in-memorynetwork create— создать тестовую сетьhost create— создать lighthouse + host → получить tokens- Start HTTP server in-process
- Agent enrollment — отправить token, получить cert
- Agent poll — проверить 304 (нет обновлений)
- Block host — добавить в blocklist
- Agent poll — проверить что blocklist обновлён
- Verify: все сертификаты валидны, config.yml корректен
Цель: веб-интерфейс для администрирования, автоматическое обновление сертификатов, полное управление lighthouse и relay.
Tech stack: Svelte (или React) SPA, embedded в Go binary через go:embed.
Страницы:
- Dashboard: обзор сети (хосты, статусы, expiring certs)
- Networks: список сетей, создание новой
- Hosts: список хостов с фильтрами, создание, enrollment code display
- Host detail: статус, сертификат, группы, firewall rules
- Lighthouses: список lighthouse с статусом, добавление нового
- Blocklist: управление заблокированными сертификатами
- Settings: API keys, CA info, server config
API расширение:
GET /api/v1/dashboard/stats # Статистика для dashboard
GET /api/v1/hosts/{id}/certificate # Детали сертификата хоста
POST /api/v1/api-keys # Создать API key
DELETE /api/v1/api-keys/{id} # Удалить API key
Аутентификация UI: session-based (cookie) с login form. Отдельно от API key auth.
Серверная часть:
- Background goroutine проверяет сертификаты каждый час
- Сертификаты с оставшимся TTL < 20% помечаются для обновления
- При следующем poll агент получает новый сертификат
Агентная часть:
- При получении нового сертификата: записать файлы, SIGHUP Nebula
- Логирование: "certificate renewed, expires at "
CA rotation support:
- API endpoint
POST /api/v1/ca/rotate— начать ротацию CA - Server генерирует новый intermediate CA
- Переходный период: оба CA в
pki.caна всех хостах - Постепенный перевыпуск: каждый хост при следующем renewal получает cert от нового CA
- После перевыпуска всех → удаление старого CA
Host creation с ролью lighthouse:
POST /api/v1/hosts
{
"name": "lighthouse-eu",
"role": "lighthouse",
"public_ip": "203.0.113.10",
"listen_port": 4242,
"nebula_ip": "192.168.100.1",
"network_id": "net_xxx"
}
Автоматическое обновление конфигураций:
При добавлении/удалении lighthouse → все хосты сети получают обновлённый config.yml с новым static_host_map и lighthouse.hosts при следующем poll.
Relay management: аналогично lighthouse, с role: "relay" и соответствующим relay.am_relay: true в config template.
Lighthouse DNS:
- Опция в network config:
lighthouse_dns: true - При включении: lighthouse config получает
lighthouse.serve_dns: true
Мониторинг lighthouse:
- Agent на lighthouse отправляет extended status: active tunnels, handshake stats
- Dashboard показывает health status lighthouse
API:
GET /api/v1/networks/{id}/firewall # Текущие правила
PUT /api/v1/networks/{id}/firewall # Обновить правила
Model:
{
"inbound": [
{"port": "any", "proto": "icmp", "groups": ["any"]},
{"port": "22", "proto": "tcp", "groups": ["admin"]}
],
"outbound": [
{"port": "any", "proto": "any", "groups": ["any"]}
]
}При обновлении правил → все хосты сети получают новый config при poll.
- IP allocation: автоматическое назначение следующего свободного IP при создании хоста (опционально, можно указать вручную)
- Groups management: CRUD для групп с привязкой к firewall rules
- Network settings: MTU, punchy, cipher, relay defaults
Каждое действие через API логируется:
CREATE TABLE audit_log (
id TEXT PRIMARY KEY,
timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
actor TEXT NOT NULL, -- API key ID or "system"
action TEXT NOT NULL, -- "host.create", "host.block", "cert.renew"
resource TEXT NOT NULL, -- host ID, network ID, etc.
details TEXT -- JSON with details
);API: GET /api/v1/audit-log?from=<date>&to=<date>&action=<action>
- Форк Mobile Nebula с pull-агентом
- QR-code enrollment (encode token + server URL)
- Deep link support
- OIDC/SSO authentication для Web UI
- RBAC — роли администраторов (viewer, operator, admin)
- Multi-user support
- PostgreSQL support
- PKCS#11/HSM support для CA key
- HashiCorp Vault integration
- Intermediate CA hierarchy (root offline + intermediate online)
- Certificate transparency log
- Prometheus metrics endpoint
- Host connectivity graph
- Certificate expiration alerts
- Lighthouse health monitoring dashboard
- Webhook notifications (Slack, Telegram)