+84
-6
@@ -2,15 +2,22 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/docs"
|
||||
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/config"
|
||||
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/grpcsrv/commander"
|
||||
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/handlers"
|
||||
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository"
|
||||
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/storage"
|
||||
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto/proto"
|
||||
"github.com/gin-gonic/gin"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
)
|
||||
@@ -39,17 +46,24 @@ func main() {
|
||||
h := handlers.New(db)
|
||||
agents := handlers.AgentsGroup{Handlers: h}
|
||||
auth := handlers.AuthGroup{Handlers: h}
|
||||
agentReg := handlers.NewAgentRegistrationGroup(h)
|
||||
|
||||
// Initialize registration tokens table
|
||||
if err := h.Repo.InitRegistrationTokens(); err != nil {
|
||||
log.Printf("Warning: failed to initialize registration tokens table: %v", err)
|
||||
}
|
||||
|
||||
// Create admin user from config if not exists
|
||||
if cfg.Admin.Admin_login != "" && cfg.Admin.Admin_password != "" {
|
||||
if !h.Repo.ExistsByLogin(cfg.Admin.Admin_login) {
|
||||
_, err := h.Repo.CreateToken(repository.TokenCreate{
|
||||
Name: cfg.Admin.Admin_name,
|
||||
LastName: cfg.Admin.Admin_last_name,
|
||||
Login: cfg.Admin.Admin_login,
|
||||
Password: cfg.Admin.Admin_password,
|
||||
PermissionView: true,
|
||||
PermissionAdmin: true,
|
||||
Name: cfg.Admin.Admin_name,
|
||||
LastName: cfg.Admin.Admin_last_name,
|
||||
Login: cfg.Admin.Admin_login,
|
||||
Password: cfg.Admin.Admin_password,
|
||||
PermissionView: true,
|
||||
PermissionManage: true,
|
||||
PermissionAdmin: true,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Warning: failed to create admin user: %v", err)
|
||||
@@ -93,6 +107,17 @@ func main() {
|
||||
agentsGroup.GET("", agents.List)
|
||||
}
|
||||
|
||||
// Agent registration
|
||||
agentRegGroup := v1.Group("/agents")
|
||||
{
|
||||
agentRegGroup.POST("/register", agentReg.Register)
|
||||
}
|
||||
agentRegTokenGroup := v1.Group("/agents")
|
||||
agentRegTokenGroup.Use(auth.AuthMiddleware(), handlers.RequireManageAgent())
|
||||
{
|
||||
agentRegTokenGroup.POST("/register-token", agentReg.CreateRegistrationToken)
|
||||
}
|
||||
|
||||
// Logs (requires view permission)
|
||||
logsGroup := v1.Group("/logs")
|
||||
logsGroup.Use(auth.AuthMiddleware(), handlers.RequireView())
|
||||
@@ -126,5 +151,58 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Start gRPC server with mTLS in background
|
||||
grpcPort := os.Getenv("GRPC_PORT")
|
||||
if grpcPort == "" {
|
||||
grpcPort = "9001"
|
||||
}
|
||||
|
||||
certDir := os.Getenv("SSL_CERT_DIR")
|
||||
if certDir == "" {
|
||||
certDir = "/var/lib/hellreign/ssl"
|
||||
}
|
||||
|
||||
certFile := certDir + "/server.crt"
|
||||
keyFile := certDir + "/server.key"
|
||||
caFile := certDir + "/ca.crt"
|
||||
|
||||
// Load server cert
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load server cert: %v", err)
|
||||
}
|
||||
|
||||
// Load CA cert for client verification
|
||||
caCert, err := os.ReadFile(caFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load CA cert: %v", err)
|
||||
}
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AppendCertsFromPEM(caCert)
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
ClientCAs: caCertPool,
|
||||
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
|
||||
grpcServer := grpc.NewServer(grpc.Creds(credentials.NewTLS(tlsConfig)))
|
||||
cmdr := commander.New()
|
||||
proto.RegisterCommanderServer(grpcServer, cmdr)
|
||||
|
||||
lis, err := net.Listen("tcp", ":"+grpcPort)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to listen on gRPC port %s: %v", grpcPort, err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Printf("gRPC server starting on port %s with mTLS", grpcPort)
|
||||
if err := grpcServer.Serve(lis); err != nil {
|
||||
log.Fatalf("gRPC server error: %v", err)
|
||||
}
|
||||
}()
|
||||
defer grpcServer.GracefulStop()
|
||||
|
||||
log.Fatal(router.Run(":8080"))
|
||||
}
|
||||
|
||||
+8
-8
@@ -2,7 +2,9 @@ FROM golang:1.26.1 as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
COPY backend/ backend/
|
||||
COPY proto/ proto/
|
||||
WORKDIR /app/backend
|
||||
ENV CGO_ENABLED=0
|
||||
ENV GIN_MODE=release
|
||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
@@ -14,11 +16,9 @@ FROM alpine:3.23.0
|
||||
|
||||
RUN apk add --no-cache curl openssl bash
|
||||
|
||||
COPY --from=builder /app/backend .
|
||||
#COPY --from=builder /app/scripts /etc/mnemosyne/scripts
|
||||
#RUN chmod +x /etc/mnemosyne/scripts/generate-certs.sh
|
||||
COPY --from=builder /app/backend/backend .
|
||||
COPY --from=builder /app/backend/scripts /etc/hellreign/scripts
|
||||
RUN chmod +x /etc/hellreign/scripts/generate-certs.sh
|
||||
|
||||
EXPOSE 8080
|
||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD [ "curl --fail http://localhost:8080/health" ]
|
||||
|
||||
CMD ["./backend"]
|
||||
# Generate certificates on container start
|
||||
ENTRYPOINT ["/bin/sh", "-c", "/etc/hellreign/scripts/generate-certs.sh ${SSL_CERT_DIR:-/var/lib/hellreign/ssl} && exec ./backend"]
|
||||
|
||||
@@ -38,6 +38,80 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/agents/register": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"agents"
|
||||
],
|
||||
"summary": "Register agent",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "CSR + token",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/internal_handlers.RegisterRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/internal_handlers.RegisterResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/agents/register-token": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"agents"
|
||||
],
|
||||
"summary": "Create registration token",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Label",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.RegistrationRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/login": {
|
||||
"post": {
|
||||
"description": "Authenticate with login and password, returns a token and permissions",
|
||||
@@ -545,6 +619,17 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.RegistrationRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"label"
|
||||
],
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.TokenCreate": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -682,6 +767,32 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"internal_handlers.RegisterRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"csr",
|
||||
"token"
|
||||
],
|
||||
"properties": {
|
||||
"csr": {
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"internal_handlers.RegisterResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ca_cert": {
|
||||
"type": "string"
|
||||
},
|
||||
"client_cert": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
|
||||
@@ -27,6 +27,80 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/agents/register": {
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"agents"
|
||||
],
|
||||
"summary": "Register agent",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "CSR + token",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/internal_handlers.RegisterRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/internal_handlers.RegisterResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/agents/register-token": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"agents"
|
||||
],
|
||||
"summary": "Create registration token",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Label",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.RegistrationRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/login": {
|
||||
"post": {
|
||||
"description": "Authenticate with login and password, returns a token and permissions",
|
||||
@@ -534,6 +608,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.RegistrationRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"label"
|
||||
],
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.TokenCreate": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -671,6 +756,32 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"internal_handlers.RegisterRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"csr",
|
||||
"token"
|
||||
],
|
||||
"properties": {
|
||||
"csr": {
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"internal_handlers.RegisterResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ca_cert": {
|
||||
"type": "string"
|
||||
},
|
||||
"client_cert": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
|
||||
@@ -26,6 +26,13 @@ definitions:
|
||||
token:
|
||||
type: string
|
||||
type: object
|
||||
gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.RegistrationRequest:
|
||||
properties:
|
||||
label:
|
||||
type: string
|
||||
required:
|
||||
- label
|
||||
type: object
|
||||
gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.TokenCreate:
|
||||
properties:
|
||||
last_name:
|
||||
@@ -118,6 +125,23 @@ definitions:
|
||||
required:
|
||||
- logs
|
||||
type: object
|
||||
internal_handlers.RegisterRequest:
|
||||
properties:
|
||||
csr:
|
||||
type: string
|
||||
token:
|
||||
type: string
|
||||
required:
|
||||
- csr
|
||||
- token
|
||||
type: object
|
||||
internal_handlers.RegisterResponse:
|
||||
properties:
|
||||
ca_cert:
|
||||
type: string
|
||||
client_cert:
|
||||
type: string
|
||||
type: object
|
||||
info:
|
||||
contact: {}
|
||||
paths:
|
||||
@@ -136,6 +160,52 @@ paths:
|
||||
summary: Get connected agents
|
||||
tags:
|
||||
- agents
|
||||
/agents/register:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: CSR + token
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/internal_handlers.RegisterRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/internal_handlers.RegisterResponse'
|
||||
summary: Register agent
|
||||
tags:
|
||||
- agents
|
||||
/agents/register-token:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: Label
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/gitea_d3m0k1d_ru_d3m0k1d_HellreigN_backend_internal_repository.RegistrationRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Create registration token
|
||||
tags:
|
||||
- agents
|
||||
/auth/login:
|
||||
post:
|
||||
consumes:
|
||||
|
||||
+3
-1
@@ -9,6 +9,7 @@ require (
|
||||
github.com/swaggo/files v1.0.1
|
||||
github.com/swaggo/gin-swagger v1.6.1
|
||||
github.com/swaggo/swag v1.16.6
|
||||
golang.org/x/crypto v0.49.0
|
||||
golang.org/x/sync v0.20.0
|
||||
google.golang.org/grpc v1.80.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
@@ -68,7 +69,6 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.41.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/arch v0.25.0 // indirect
|
||||
golang.org/x/crypto v0.49.0 // indirect
|
||||
golang.org/x/mod v0.34.0 // indirect
|
||||
golang.org/x/net v0.52.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
@@ -80,3 +80,5 @@ require (
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
|
||||
replace gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto => ../proto
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto v0.0.0-20260403210401-a6212c89fc0e h1:Z/2Mjc9NU0CWIduj8Wd9ClnZt4dqmQUUXl1VlyGQe6U=
|
||||
gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto v0.0.0-20260403210401-a6212c89fc0e/go.mod h1:1DByetpOnW2+AjM8ZWbJ1Xfzprus8fBie2AMUP/YHHA=
|
||||
github.com/ClickHouse/ch-go v0.71.0 h1:bUdZ/EZj/LcVHsMqaRUP2holqygrPWQKeMjc6nZoyRM=
|
||||
github.com/ClickHouse/ch-go v0.71.0/go.mod h1:NwbNc+7jaqfY58dmdDUbG4Jl22vThgx1cYjBw0vtgXw=
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.44.0 h1:9pxs5pRwIvhni5BDRPn/n5A8DeUod5TnBaeulFBX8EQ=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
set -e
|
||||
|
||||
CERT_DIR="${1:-/etc/mnemosyne/ssl}"
|
||||
CERT_DIR="${1:-/etc/HellreigN/ssl}"
|
||||
DAYS_VALID=365
|
||||
|
||||
echo "Generating CA and server certificates in ${CERT_DIR}..."
|
||||
@@ -26,7 +26,7 @@ openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out "${CERT_DIR}/c
|
||||
openssl req -x509 -new -nodes -sha256 -days ${DAYS_VALID} \
|
||||
-key "${CERT_DIR}/ca.key" \
|
||||
-out "${CERT_DIR}/ca.crt" \
|
||||
-subj "/CN=Mnemosyne Root CA"
|
||||
-subj "/CN=HellreigN Root CA"
|
||||
|
||||
# Генерация серверного сертификата
|
||||
echo "Generating server certificate..."
|
||||
|
||||
Reference in New Issue
Block a user