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
+9 -5
View File
@@ -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"]
+1
View File
@@ -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 (
+4
View File
@@ -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=
+32
View File
@@ -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
}
+49
View File
@@ -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)
}
+169
View File
@@ -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(&regResp); 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
}
+48
View File
@@ -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")
}