+9
-5
@@ -2,16 +2,20 @@ FROM golang:1.26.1 as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
COPY agent/ agent/
|
||||
COPY proto/ proto/
|
||||
WORKDIR /app/agent
|
||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=cache,target=/root/.cache/go-build \
|
||||
go mod download
|
||||
|
||||
COPY . .
|
||||
ENV CGO_ENABLED=0
|
||||
COPY agent/ agent/
|
||||
COPY proto/ proto/
|
||||
WORKDIR /app/agent
|
||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=cache,target=/root/.cache/go-build \
|
||||
go build -ldflags "-s -w" -o agent ./main.go
|
||||
go mod tidy && \
|
||||
CGO_ENABLED=0 go build -ldflags "-s -w" -o agent ./main.go
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
@@ -21,6 +25,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/agent .
|
||||
COPY --from=builder /app/agent/agent .
|
||||
|
||||
CMD ["./agent"]
|
||||
|
||||
@@ -6,6 +6,7 @@ require (
|
||||
gitea.d3m0k1d.ru/d3m0k1d/HellreigN/proto v0.0.0-20260403214837-94be9799f47d
|
||||
golang.org/x/sync v0.20.0
|
||||
google.golang.org/grpc v1.80.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
@@ -40,3 +40,7 @@ google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type AgentConfig struct {
|
||||
BackendURL string `yaml:"backend_url"`
|
||||
RegistrationToken string `yaml:"registration_token"`
|
||||
Label string `yaml:"label"`
|
||||
CertDir string `yaml:"cert_dir"`
|
||||
}
|
||||
|
||||
func Load(path string) (*AgentConfig, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var cfg AgentConfig
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cfg.CertDir == "" {
|
||||
cfg.CertDir = "/etc/hellreign-agent/certs"
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package mtls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
// LoadMTLSCredentials loads client certificate and CA certificate for mTLS.
|
||||
func LoadMTLSCredentials(caCertPEM, clientCertPEM, clientKeyPEM []byte) (credentials.TransportCredentials, error) {
|
||||
cert, err := tls.X509KeyPair(clientCertPEM, clientKeyPEM)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load client key pair: %w", err)
|
||||
}
|
||||
|
||||
caCertPool := x509.NewCertPool()
|
||||
if !caCertPool.AppendCertsFromPEM(caCertPEM) {
|
||||
return nil, fmt.Errorf("failed to append CA certificate")
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
RootCAs: caCertPool,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
|
||||
return credentials.NewTLS(tlsConfig), nil
|
||||
}
|
||||
|
||||
// LoadMTLSCredentialsFromFiles loads mTLS credentials from file paths.
|
||||
func LoadMTLSCredentialsFromFiles(caCertPath, clientCertPath, clientKeyPath string) (credentials.TransportCredentials, error) {
|
||||
caCert, err := os.ReadFile(caCertPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read CA cert: %w", err)
|
||||
}
|
||||
clientCert, err := os.ReadFile(clientCertPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read client cert: %w", err)
|
||||
}
|
||||
clientKey, err := os.ReadFile(clientKeyPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read client key: %w", err)
|
||||
}
|
||||
|
||||
return LoadMTLSCredentials(caCert, clientCert, clientKey)
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package registration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Certs struct {
|
||||
CACertPEM []byte
|
||||
ClientCertPEM []byte
|
||||
ClientKeyPEM []byte
|
||||
}
|
||||
|
||||
type RegisterRequest struct {
|
||||
CSR string `json:"csr"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type RegisterResponse struct {
|
||||
CACert string `json:"ca_cert"`
|
||||
ClientCert string `json:"client_cert"`
|
||||
}
|
||||
|
||||
type TokenResponse struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type ErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// GenerateKeyAndCSR generates a new ECDSA private key and CSR for the agent.
|
||||
func GenerateKeyAndCSR(label string) (*ecdsa.PrivateKey, []byte, error) {
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("generate key: %w", err)
|
||||
}
|
||||
|
||||
template := x509.CertificateRequest{
|
||||
Subject: pkix.Name{
|
||||
CommonName: label,
|
||||
Organization: []string{"HellreigN Agent"},
|
||||
},
|
||||
SignatureAlgorithm: x509.ECDSAWithSHA256,
|
||||
}
|
||||
|
||||
csrDER, err := x509.CreateCertificateRequest(rand.Reader, &template, key)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("create csr: %w", err)
|
||||
}
|
||||
|
||||
csrPEM := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE REQUEST",
|
||||
Bytes: csrDER,
|
||||
})
|
||||
|
||||
return key, csrPEM, nil
|
||||
}
|
||||
|
||||
// Register sends CSR to backend and receives signed certificates.
|
||||
func Register(backendURL, token string, csrPEM []byte) (*Certs, error) {
|
||||
reqBody := RegisterRequest{CSR: string(csrPEM), Token: token}
|
||||
body, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/api/v1/agents/register", backendURL)
|
||||
resp, err := http.Post(url, "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("register request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
var errResp ErrorResponse
|
||||
json.NewDecoder(resp.Body).Decode(&errResp)
|
||||
return nil, fmt.Errorf("registration failed (status %d): %s", resp.StatusCode, errResp.Error)
|
||||
}
|
||||
|
||||
var regResp RegisterResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(®Resp); err != nil {
|
||||
return nil, fmt.Errorf("decode response: %w", err)
|
||||
}
|
||||
|
||||
return &Certs{
|
||||
CACertPEM: []byte(regResp.CACert),
|
||||
ClientCertPEM: []byte(regResp.ClientCert),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SaveCerts saves CA cert, client cert, and client key to the given directory.
|
||||
func SaveCerts(certDir string, certs *Certs, key *ecdsa.PrivateKey) error {
|
||||
if err := os.MkdirAll(certDir, 0700); err != nil {
|
||||
return fmt.Errorf("create cert dir: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filepath.Join(certDir, "ca.crt"), certs.CACertPEM, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(certDir, "client.crt"), certs.ClientCertPEM, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keyDER, err := x509.MarshalECPrivateKey(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keyPEM := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "EC PRIVATE KEY",
|
||||
Bytes: keyDER,
|
||||
})
|
||||
if err := os.WriteFile(filepath.Join(certDir, "client.key"), keyPEM, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadCerts loads existing certificates and key from disk.
|
||||
func LoadCerts(certDir string) (*Certs, *ecdsa.PrivateKey, error) {
|
||||
caCert, err := os.ReadFile(filepath.Join(certDir, "ca.crt"))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
clientCert, err := os.ReadFile(filepath.Join(certDir, "client.crt"))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
clientKeyPEM, err := os.ReadFile(filepath.Join(certDir, "client.key"))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(clientKeyPEM)
|
||||
if block == nil {
|
||||
return nil, nil, fmt.Errorf("decode client key")
|
||||
}
|
||||
key, err := x509.ParseECPrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("parse client key: %w", err)
|
||||
}
|
||||
|
||||
return &Certs{
|
||||
CACertPEM: caCert,
|
||||
ClientCertPEM: clientCert,
|
||||
}, key, nil
|
||||
}
|
||||
|
||||
// CertsExist checks if all certificate files exist in the directory.
|
||||
func CertsExist(certDir string) bool {
|
||||
files := []string{"ca.crt", "client.crt", "client.key"}
|
||||
for _, f := range files {
|
||||
if _, err := os.Stat(filepath.Join(certDir, f)); err != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1 +1,49 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/agent/internal/config"
|
||||
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/agent/internal/registration"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfgPath := os.Getenv("CONFIG_FILE")
|
||||
if cfgPath == "" {
|
||||
cfgPath = "/etc/hellreign-agent/config.yml"
|
||||
}
|
||||
|
||||
cfg, err := config.Load(cfgPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Agent label: %s", cfg.Label)
|
||||
|
||||
if cfg.RegistrationToken == "" {
|
||||
log.Fatal("No registration token provided")
|
||||
}
|
||||
|
||||
// Generate key and CSR
|
||||
key, csrPEM, err := registration.GenerateKeyAndCSR(cfg.Label)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to generate key and CSR: %v", err)
|
||||
}
|
||||
log.Println("Generated ECDSA key pair and CSR")
|
||||
|
||||
// Register with backend
|
||||
certs, err := registration.Register(cfg.BackendURL, cfg.RegistrationToken, csrPEM)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to register: %v", err)
|
||||
}
|
||||
log.Println("Successfully registered, received certificates")
|
||||
|
||||
// Save certificates
|
||||
if err := registration.SaveCerts(cfg.CertDir, certs, key); err != nil {
|
||||
log.Fatalf("Failed to save certificates: %v", err)
|
||||
}
|
||||
log.Printf("Certificates saved to %s", cfg.CertDir)
|
||||
|
||||
log.Println("Agent registration complete")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user