Найбільше вузьке місце до запуску: агент заводився 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>
67 lines
3 KiB
Go
67 lines
3 KiB
Go
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
|
||
}
|