Один коміт, а не десяток тематичних, свідомо: теми переплетені в
спільних файлах (store.go, docker-compose.yml, deploy/README.md), і
розділити їх можна було б лише індексуванням шматків. Коміти, які не
збираються, гірші за один великий — тим паче що це рівно той стан, який
перевірявся разом.
ЩО ПРАЦЮЄ НА СТЕНДІ Й ПЕРЕВІРЕНО ТАМ
0058 подієві алерти: syslog, ncm, compliance спрацьовують у мить
події; правило з нереалізованим джерелом більше не зберігається
мовчки
0059 snmp.walk і прототипи шаблонів — таблиці з динамічним індексом
описуються шаблоном, а не Go
0060 відкат конфігу: план як різниця, маскування паролів із підписом
плану, обов'язковий контрольний збір, verifying при обриві
0061 кнопки Telegram: довге опитування, авторизація не з callback_data
0062 аудит і архів хостів; тест на AST, що падає на ключі без назви
0063 RLS: три ролі, окремий пул для фонових тактів
0064 строки зберігання даних і сторінка сховища
0065 приймач SNMP-трапів; перевірено справжніми пакетами по дроту,
переклад v1→v2 за RFC 3584 дає правильний OID
0066 ескалації сповіщень
0067 алерт про вичерпання диска
0068 поля заливки конфігу переїхали в каталог профілів
Плюс: 137 тестів вебу з нуля (їх не було взагалі), одинадцять справжніх
вад, знайдених ними й виправлених, і виправлення двох інтеграційних
тестів grpcapi, які мовчки пропускались півтора року.
ЩО ЩЕ НЕ ЗАПУСКАЛОСЬ
netpulse установник: одна команда замість 18 змінних і
593 рядків інструкції
RLS з першого запуску нова інсталяція під політиками одразу;
RLS-EXISTING-INSTALL.md лишається тільки для
старих інсталяцій
.forgejo + CI раннер не зареєстрований
Ці три перевірені компіляцією й міркуванням, але не виконанням.
ГОЛОВНИЙ ВИСНОВОК ДВОХ СЕСІЙ
Зелена перевірка доводить рівно те, що вона перевіряє. Тест ізоляції RLS
був правильний і зелений — і пропустив зламаний вхід, бо перевіряв «чи
не видно чужого», коли зламалось «чи видно своє». Інтеграційні тести
grpcapi були зелені, бо не виконувались. Схема, довідник і протокол
описували те, чого в коді не існувало, і виглядало це як готове.
Тому в кожному завданні цих сесій стояла вимога назвати НЕПОКРИТЕ, а
чотири задачі закінчились не можливістю, а відмовою: правило з
нереалізованим джерелом не зберігається, профіль без команд заливки
каже про це замість мовчазної кнопки, міграція RLS валить сама себе на
таблиці без політики, тест словника аудиту падає на ключі без назви.
Подробиці — HISTORY.md, розділи за 26 і 27 серпня.
215 lines
9.1 KiB
Go
215 lines
9.1 KiB
Go
// Package config — налаштування агента з прапорців і змінних оточення.
|
||
//
|
||
// Конфіг-файлу навмисно немає: у зонда рівно жменя параметрів, а
|
||
// відсутність YAML економить залежність і кілька сотень кілобайт
|
||
// бінарника. Усе інше агент отримує від сервера в Welcome і ModuleControl.
|
||
package config
|
||
|
||
import (
|
||
"crypto/tls"
|
||
"crypto/x509"
|
||
"errors"
|
||
"flag"
|
||
"fmt"
|
||
"os"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/netpulse/netpulse/agent/internal/localfiles"
|
||
)
|
||
|
||
type Config struct {
|
||
// Куди підключатись: host:port.
|
||
Endpoint string
|
||
AgentID string
|
||
Hostname string
|
||
// Токен зонда, виданий при Enroll. Визначає, ЯКИЙ це зонд;
|
||
// сертифікат mTLS — що він узагалі має право говорити з сервером.
|
||
Token string
|
||
|
||
// mTLS. Порожні шляхи + Insecure = локальний стенд без шифрування.
|
||
CertFile string
|
||
KeyFile string
|
||
CAFile string
|
||
Insecure bool
|
||
|
||
MaxConcurrency int
|
||
BufferMaxItems int
|
||
BufferMaxBytes int
|
||
|
||
MinBackoff time.Duration
|
||
MaxBackoff time.Duration
|
||
|
||
LogLevel string
|
||
LogJSON bool
|
||
|
||
// Модулі, увімкнені до першої відповіді сервера.
|
||
DefaultModules []string
|
||
|
||
// Адреса приймача syslog. Порт нижче 1024 вимагає
|
||
// CAP_NET_BIND_SERVICE — тому й налаштовується: віддати зонду це
|
||
// право можна не всюди, а перенаправити 514 на 5514 правилом
|
||
// фаєрвола можна скрізь.
|
||
SyslogListen string
|
||
|
||
// Адреса приймача SNMP-трапів. Той самий випадок із
|
||
// CAP_NET_BIND_SERVICE, що й у syslog: 162 — привілейований порт.
|
||
TrapsListen string
|
||
|
||
// Одноразовий токен запрошення. Якщо заданий — зонд спершу
|
||
// реєструється, зберігає посвідчення й лише потім працює.
|
||
EnrollToken string
|
||
// Бажане ім'я зонда при реєстрації.
|
||
EnrollName string
|
||
// Де лежить посвідчення між запусками.
|
||
IdentityPath string
|
||
|
||
// Перелік конфіг-файлів, які вільно читати з ЦІЄЇ машини.
|
||
//
|
||
// Єдине налаштування зонда, яке принципово не може приходити з
|
||
// сервера. Зонд читає файли правами машини, і поле «які саме»,
|
||
// кероване з кабінету, дало б будь-кому з правом ncm:write читання
|
||
// довільного файлу на всіх серверах клієнта одразу. Тому перелік
|
||
// лежить у файлі на самій машині — там, де його редагує той, хто
|
||
// вже має на ній права, — а сервер про його вміст не знає нічого.
|
||
//
|
||
// Прапорець задає лише ШЛЯХ до переліку, і сам по собі нічого не
|
||
// дозволяє: без файлу збір локальних конфігів не робиться взагалі.
|
||
FileCfgPolicy string
|
||
}
|
||
|
||
func envOr(key, def string) string {
|
||
if v := os.Getenv(key); v != "" {
|
||
return v
|
||
}
|
||
return def
|
||
}
|
||
|
||
func envInt(key string, def int) int {
|
||
if v := os.Getenv(key); v != "" {
|
||
if n, err := strconv.Atoi(v); err == nil {
|
||
return n
|
||
}
|
||
}
|
||
return def
|
||
}
|
||
|
||
// Parse читає прапорці поверх змінних оточення (NETPULSE_*).
|
||
func Parse(args []string) (*Config, error) {
|
||
host, _ := os.Hostname()
|
||
|
||
c := &Config{}
|
||
fs := flag.NewFlagSet("netpulse-agent", flag.ContinueOnError)
|
||
|
||
fs.StringVar(&c.Endpoint, "server", envOr("NETPULSE_SERVER", ""), "адреса сервера, host:port")
|
||
fs.StringVar(&c.AgentID, "agent-id", envOr("NETPULSE_AGENT_ID", ""), "ідентифікатор зонда")
|
||
fs.StringVar(&c.Hostname, "hostname", envOr("NETPULSE_HOSTNAME", host), "ім'я хоста для журналу сервера")
|
||
|
||
fs.StringVar(&c.Token, "token", envOr("NETPULSE_TOKEN", ""), "токен зонда з Enroll")
|
||
fs.StringVar(&c.CertFile, "cert", envOr("NETPULSE_CERT", ""), "клієнтський сертифікат (mTLS)")
|
||
fs.StringVar(&c.KeyFile, "key", envOr("NETPULSE_KEY", ""), "приватний ключ")
|
||
fs.StringVar(&c.CAFile, "ca", envOr("NETPULSE_CA", ""), "CA сервера")
|
||
fs.BoolVar(&c.Insecure, "insecure", os.Getenv("NETPULSE_INSECURE") == "1",
|
||
"без TLS — лише для локального стенду")
|
||
|
||
fs.IntVar(&c.MaxConcurrency, "max-concurrency", envInt("NETPULSE_MAX_CONCURRENCY", 64),
|
||
"скільки чеків виконувати паралельно")
|
||
fs.IntVar(&c.BufferMaxItems, "buffer-items", envInt("NETPULSE_BUFFER_ITEMS", 50_000),
|
||
"ліміт записів у буфері телеметрії")
|
||
fs.IntVar(&c.BufferMaxBytes, "buffer-bytes", envInt("NETPULSE_BUFFER_BYTES", 8<<20),
|
||
"оціночний ліміт пам'яті буфера в байтах")
|
||
|
||
fs.DurationVar(&c.MinBackoff, "min-backoff", time.Second, "мінімальна пауза перед реконектом")
|
||
fs.DurationVar(&c.MaxBackoff, "max-backoff", 2*time.Minute, "максимальна пауза перед реконектом")
|
||
|
||
fs.StringVar(&c.LogLevel, "log-level", envOr("NETPULSE_LOG_LEVEL", "info"), "debug|info|warn|error")
|
||
fs.BoolVar(&c.LogJSON, "log-json", os.Getenv("NETPULSE_LOG_JSON") == "1", "журнал у JSON")
|
||
|
||
mods := fs.String("modules", envOr("NETPULSE_MODULES", "icmp"),
|
||
"модулі, увімкнені до відповіді сервера (через кому)")
|
||
|
||
fs.StringVar(&c.SyslogListen, "syslog-listen", envOr("NETPULSE_SYSLOG_LISTEN", ":514"),
|
||
"адреса приймача syslog; діє лише з модулем syslog")
|
||
|
||
fs.StringVar(&c.TrapsListen, "traps-listen", envOr("NETPULSE_TRAPS_LISTEN", ":162"),
|
||
"адреса приймача SNMP-трапів; діє лише з модулем traps")
|
||
|
||
fs.StringVar(&c.EnrollToken, "enroll", envOr("NETPULSE_ENROLL", ""),
|
||
"одноразовий токен запрошення з UI")
|
||
fs.StringVar(&c.EnrollName, "name", envOr("NETPULSE_NAME", ""),
|
||
"бажане ім'я зонда при реєстрації")
|
||
fs.StringVar(&c.IdentityPath, "identity", envOr("NETPULSE_IDENTITY", DefaultIdentityPath()),
|
||
"файл посвідчення зонда")
|
||
|
||
fs.StringVar(&c.FileCfgPolicy, "files-policy",
|
||
envOr("NETPULSE_FILES_POLICY", localfiles.DefaultPolicyPath()),
|
||
"перелік конфіг-файлів цієї машини, дозволених до збору")
|
||
|
||
if err := fs.Parse(args); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
for _, m := range strings.Split(*mods, ",") {
|
||
if m = strings.TrimSpace(m); m != "" {
|
||
c.DefaultModules = append(c.DefaultModules, m)
|
||
}
|
||
}
|
||
|
||
return c, c.validate()
|
||
}
|
||
|
||
func (c *Config) validate() error {
|
||
if c.Endpoint == "" {
|
||
return errors.New("не вказано адресу сервера (-server або NETPULSE_SERVER)")
|
||
}
|
||
// Ідентичність тут не перевіряємо взагалі.
|
||
//
|
||
// До реєстрації її ще немає, після — вона приходить із файлу
|
||
// посвідчення, про який validate не знає. Перевірка живе в main,
|
||
// коли всі три джерела (прапорці, файл, реєстрація) вже враховані:
|
||
// інакше запуск із самим -identity падав із «зонд не
|
||
// зареєстрований», хоча посвідчення лежало поруч.
|
||
if !c.Insecure && (c.CertFile == "" || c.KeyFile == "") {
|
||
return errors.New("потрібні -cert і -key для mTLS (або -insecure для локального стенду)")
|
||
}
|
||
if c.MaxConcurrency <= 0 {
|
||
return errors.New("-max-concurrency має бути додатним")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// TLSConfig збирає mTLS-конфігурацію.
|
||
//
|
||
// Перевірка сертифіката сервера обов'язкова й не вимикається прапорцем:
|
||
// зонд ходить через інтернет, і «тимчасово без перевірки» — це той
|
||
// самий канал, яким їдуть паролі від усіх комутаторів клієнта.
|
||
func (c *Config) TLSConfig() (*tls.Config, error) {
|
||
if c.Insecure {
|
||
return nil, nil
|
||
}
|
||
|
||
cert, err := tls.LoadX509KeyPair(c.CertFile, c.KeyFile)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("клієнтський сертифікат: %w", err)
|
||
}
|
||
|
||
tc := &tls.Config{
|
||
Certificates: []tls.Certificate{cert},
|
||
MinVersion: tls.VersionTLS13,
|
||
}
|
||
|
||
if c.CAFile != "" {
|
||
pem, err := os.ReadFile(c.CAFile)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("CA: %w", err)
|
||
}
|
||
pool := x509.NewCertPool()
|
||
if !pool.AppendCertsFromPEM(pem) {
|
||
return nil, errors.New("CA: не вдалося розібрати PEM")
|
||
}
|
||
tc.RootCAs = pool
|
||
}
|
||
|
||
return tc, nil
|
||
}
|