@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user