chore: grpc + mtls working
ci-agent / build (push) Failing after 1m19s

This commit is contained in:
d3m0k1d
2026-04-04 03:55:37 +03:00
parent 28631865c8
commit a2c71da3a0
24 changed files with 1095 additions and 31 deletions
@@ -17,6 +17,12 @@ type Commander struct {
agents map[string]Agent
}
func New() *Commander {
return &Commander{
agents: make(map[string]Agent),
}
}
type Agent struct {
bidi grpc.BidiStreamingServer[proto.FinishedCommand, proto.Command]
in chan *proto.Command
+121
View File
@@ -0,0 +1,121 @@
package handlers
import (
"log"
"net/http"
"os"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/utils"
"github.com/gin-gonic/gin"
)
type AgentRegistrationGroup struct {
*Handlers
certBundle *utils.CertBundle
}
func NewAgentRegistrationGroup(h *Handlers) *AgentRegistrationGroup {
certDir := getCertDir()
bundle, err := utils.LoadCertBundle(certDir)
if err != nil {
log.Printf("[agent-reg] WARNING: cert bundle load failed: %v", err)
}
return &AgentRegistrationGroup{
Handlers: h,
certBundle: bundle,
}
}
// CreateRegistrationToken — админ создаёт токен для агента
// @Summary Create registration token
// @Tags agents
// @Accept json
// @Produce json
// @Param request body repository.RegistrationRequest true "Label"
// @Success 200 {object} map[string]string
// @Security Bearer
// @Router /agents/register-token [post]
func (arg *AgentRegistrationGroup) CreateRegistrationToken(c *gin.Context) {
var req repository.RegistrationRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
token, err := arg.Repo.CreateRegistrationToken(req.Label)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create token"})
return
}
c.JSON(http.StatusOK, gin.H{"token": token})
}
// Register — агент шлёт CSR + token, получает сертификаты
// @Summary Register agent
// @Tags agents
// @Accept json
// @Produce json
// @Param request body RegisterRequest true "CSR + token"
// @Success 200 {object} RegisterResponse
// @Router /agents/register [post]
func (arg *AgentRegistrationGroup) Register(c *gin.Context) {
var req RegisterRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if arg.certBundle == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "certificate bundle not available"})
return
}
regToken, err := arg.Repo.GetRegistrationToken(req.Token)
if err != nil {
if err == repository.ErrNotFound {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid registration token"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to verify token"})
return
}
if regToken.Used {
c.JSON(http.StatusGone, gin.H{"error": "registration token already used"})
return
}
clientCertPEM, err := arg.certBundle.SignCSR([]byte(req.CSR), regToken.Label)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "failed to sign CSR: " + err.Error()})
return
}
if err := arg.Repo.MarkRegistrationTokenUsed(req.Token); err != nil {
log.Printf("[agent-reg] WARNING: failed to mark token used: %v", err)
}
c.JSON(http.StatusOK, RegisterResponse{
CACert: string(arg.certBundle.GetCACertPEM()),
ClientCert: string(clientCertPEM),
})
}
type RegisterRequest struct {
CSR string `json:"csr" binding:"required"`
Token string `json:"token" binding:"required"`
}
type RegisterResponse struct {
CACert string `json:"ca_cert"`
ClientCert string `json:"client_cert"`
}
func getCertDir() string {
if d := os.Getenv("SSL_CERT_DIR"); d != "" {
return d
}
return "/var/lib/hellreign/ssl"
}
+21
View File
@@ -39,3 +39,24 @@ type LoginResponse struct {
PermissionManage bool `json:"permission_manage_agent"`
PermissionAdmin bool `json:"permission_admin"`
}
// RegistrationToken represents a one-time agent registration token.
type RegistrationToken struct {
ID int64 `json:"id"`
Token string `json:"token"`
Label string `json:"label"`
Used bool `json:"used"`
CreatedAt *string `json:"created_at"`
UsedAt *string `json:"used_at"`
}
// RegistrationRequest is the request body for creating a registration token.
type RegistrationRequest struct {
Label string `json:"label" binding:"required"`
}
// RegistrationResponse is returned when an agent registers.
type RegistrationResponse struct {
CACert string `json:"ca_cert"`
ClientCert string `json:"client_cert"`
}
+59
View File
@@ -185,3 +185,62 @@ func (r *Repository) ExistsByLogin(login string) bool {
}
return count > 0
}
// InitRegistrationTokens creates the registration_tokens table if it does not exist.
func (r *Repository) InitRegistrationTokens() error {
_, err := r.DB.Exec(storage.CreateRegistrationTokensTable)
return err
}
// CreateRegistrationToken inserts a new one-time registration token.
func (r *Repository) CreateRegistrationToken(label string) (string, error) {
token, err := utils.RandomToken()
if err != nil {
return "", err
}
_, err = r.DB.Exec(
`INSERT INTO registration_tokens (token, label, used) VALUES (?, ?, 0)`,
token, label,
)
if err != nil {
return "", err
}
return token, nil
}
// GetRegistrationToken retrieves a registration token if it exists and is not used.
func (r *Repository) GetRegistrationToken(token string) (*RegistrationToken, error) {
var rt RegistrationToken
err := r.DB.QueryRow(
`SELECT id, token, label, used, created_at, used_at FROM registration_tokens WHERE token = ?`,
token,
).Scan(&rt.ID, &rt.Token, &rt.Label, &rt.Used, &rt.CreatedAt, &rt.UsedAt)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, err
}
return &rt, nil
}
// MarkRegistrationTokenUsed marks a registration token as used.
func (r *Repository) MarkRegistrationTokenUsed(token string) error {
result, err := r.DB.Exec(
`UPDATE registration_tokens SET used = 1, used_at = CURRENT_TIMESTAMP WHERE token = ? AND used = 0`,
token,
)
if err != nil {
return err
}
affected, err := result.RowsAffected()
if err != nil {
return err
}
if affected == 0 {
return ErrNotFound
}
return nil
}
+11
View File
@@ -14,6 +14,17 @@ const CreateSqlite = `
);
`
const CreateRegistrationTokensTable = `
CREATE TABLE IF NOT EXISTS registration_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
token TEXT NOT NULL UNIQUE,
label TEXT NOT NULL,
used BOOL NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
used_at DATETIME
);
`
const CreateLogsTable = `
CREATE TABLE IF NOT EXISTS logs (
timestamp DateTime64(3) DEFAULT now(),
+157
View File
@@ -0,0 +1,157 @@
package utils
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"os"
"path/filepath"
"time"
)
// CertBundle holds CA and server certificates loaded from disk.
type CertBundle struct {
CACert *x509.Certificate
CAKey *rsa.PrivateKey
ServerCert *x509.Certificate
ServerKey *rsa.PrivateKey
}
// LoadCertBundle loads CA and server certificates from the given directory.
func LoadCertBundle(certDir string) (*CertBundle, error) {
caCertPEM, err := os.ReadFile(filepath.Join(certDir, "ca.crt"))
if err != nil {
return nil, fmt.Errorf("read ca.crt: %w", err)
}
caKeyPEM, err := os.ReadFile(filepath.Join(certDir, "ca.key"))
if err != nil {
return nil, fmt.Errorf("read ca.key: %w", err)
}
serverCertPEM, err := os.ReadFile(filepath.Join(certDir, "server.crt"))
if err != nil {
return nil, fmt.Errorf("read server.crt: %w", err)
}
serverKeyPEM, err := os.ReadFile(filepath.Join(certDir, "server.key"))
if err != nil {
return nil, fmt.Errorf("read server.key: %w", err)
}
caCert := decodeCert(caCertPEM)
caKey, err := decodeRSAPrivateKey(caKeyPEM)
if err != nil {
return nil, fmt.Errorf("parse ca.key: %w", err)
}
serverCert := decodeCert(serverCertPEM)
serverKey, err := decodeRSAPrivateKey(serverKeyPEM)
if err != nil {
return nil, fmt.Errorf("parse server.key: %w", err)
}
return &CertBundle{
CACert: caCert,
CAKey: caKey,
ServerCert: serverCert,
ServerKey: serverKey,
}, nil
}
// SignCSR signs a client CSR with the CA and returns the client certificate PEM.
func (b *CertBundle) SignCSR(csrPEM []byte, label string) ([]byte, error) {
csr := decodeCSR(csrPEM)
// Verify CSR signature
if err := csr.CheckSignature(); err != nil {
return nil, fmt.Errorf("invalid CSR signature: %w", err)
}
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil {
return nil, fmt.Errorf("generate serial: %w", err)
}
now := time.Now()
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
CommonName: label,
Organization: csr.Subject.Organization,
},
NotBefore: now,
NotAfter: now.Add(365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
}
certDER, err := x509.CreateCertificate(rand.Reader, &template, b.CACert, csr.PublicKey, b.CAKey)
if err != nil {
return nil, fmt.Errorf("create certificate: %w", err)
}
certPEM := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: certDER,
})
return certPEM, nil
}
// GetCACertPEM returns the CA certificate as PEM bytes.
func (b *CertBundle) GetCACertPEM() []byte {
return pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: b.CACert.Raw,
})
}
func decodeCert(pemData []byte) *x509.Certificate {
block, _ := pem.Decode(pemData)
if block == nil {
return nil
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil
}
return cert
}
func decodeRSAPrivateKey(pemData []byte) (*rsa.PrivateKey, error) {
block, _ := pem.Decode(pemData)
if block == nil {
return nil, fmt.Errorf("no PEM block found")
}
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
// Try PKCS1 fallback
key, err = x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("parse PKCS1: %w", err)
}
return key.(*rsa.PrivateKey), nil
}
rsaKey, ok := key.(*rsa.PrivateKey)
if !ok {
return nil, fmt.Errorf("key is not RSA, got %T", key)
}
return rsaKey, nil
}
func decodeCSR(pemData []byte) *x509.CertificateRequest {
block, _ := pem.Decode(pemData)
if block == nil {
return nil
}
csr, err := x509.ParseCertificateRequest(block.Bytes)
if err != nil {
return nil
}
return csr
}