Netpulse_SasS/server/internal/grpcapi/enroll.go
byrsapty ddaae60fa0 Етап 8: реєстрація зонда одноразовим запрошенням
Найбільше вузьке місце до запуску: агент заводився INSERT-ом у базу, а
токен вписувався в командний рядок руками. Поставити зонд у клієнта було
неможливо.

core.agent_enrollments тримає sha256 одноразового токена; сам токен
повертається рівно один раз. Видача під FOR UPDATE в одній транзакції:
два агенти з однієї скопійованої команди інакше створили б два зонди з
одного запрошення. Відповідь на «немає», «згоріло» і «використано»
однакова — розрізняти їх означає підказувати тому, хто підбирає токени.

Токен зонда їде окремим полем agent_token, а не в certificate:
сертифікат відповідає на інше питання й живе за іншим циклом.

Агент зберігає посвідчення в /etc/netpulse/agent.json з правами 0600,
через тимчасовий файл і перейменування — обрив живлення посеред запису
інакше лишив би половину токена.

Знайдено живим прогоном: реєстрація не проходила автентифікацію, бо
інтерсептор стоїть на всьому сервері, а не на окремому сервісі — мій же
коментар стверджував протилежне. І запуск із самим посвідченням падав:
validate() вимагав -agent-id, не знаючи про файл.

Сторінка зондів: команда встановлення з токеном, відкликання
запрошень, керування модулями й лімітами, видалення.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 00:50:57 +03:00

67 lines
3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package grpcapi
import (
"context"
"errors"
"strings"
"github.com/netpulse/netpulse/server/internal/store"
npv1 "github.com/netpulse/netpulse/gen/go/netpulse/v1"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// EnrollService реалізує npv1.EnrollmentServiceServer.
//
// Окремий тип, а не метод на Service: Enroll — єдиний виклик, який іде
// БЕЗ токена зонда, тож він не має проходити через інтерсептор
// автентифікації AgentService. Різні сервіси в одному gRPC-сервері
// дозволяють це розвести без винятків усередині інтерсептора.
type EnrollService struct {
npv1.UnimplementedEnrollmentServiceServer
store *store.Store
// Куди підключатись після реєстрації. Може відрізнятись від адреси,
// на яку прийшов Enroll: балансувальник, регіональний шлюз.
controlEndpoint string
}
func NewEnrollService(st *store.Store, controlEndpoint string) *EnrollService {
return &EnrollService{store: st, controlEndpoint: controlEndpoint}
}
// Enroll обмінює одноразове запрошення на постійний зонд.
//
// Сертифікат у відповіді поки порожній. mTLS у продукті є (сервер
// приймає -client-ca), але він налаштовується окремо від реєстрації:
// власний CA з ротацією — це друга система, і вдавати, що вона вже
// працює, означало б віддати агенту порожній сертифікат і зламати
// з'єднання там, де воно щойно працювало.
func (s *EnrollService) Enroll(ctx context.Context, req *npv1.EnrollRequest) (*npv1.EnrollResponse, error) {
token := strings.TrimSpace(req.GetEnrollmentToken())
if token == "" {
return nil, status.Error(codes.InvalidArgument, "потрібен токен запрошення")
}
b := req.GetBuild()
agent, err := s.store.RedeemEnrollment(ctx, token,
req.GetHostname(), req.GetRequestedName(),
b.GetVersion(), b.GetOs(), b.GetArch())
if errors.Is(err, store.ErrEnrollmentInvalid) {
// Одна відповідь на «немає», «згоріло» і «вже використано»:
// розрізняти їх означало б підказувати, наскільки близько той,
// хто підбирає токени.
return nil, status.Error(codes.PermissionDenied, "запрошення недійсне")
}
if err != nil {
return nil, status.Errorf(codes.Internal, "реєстрація: %v", err)
}
return &npv1.EnrollResponse{
AgentId: agent.ID,
AgentName: agent.Name,
ControlEndpoint: s.controlEndpoint,
AgentToken: agent.Token,
}, nil
}