Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ certificates:
# ca-cert is the certificate of the CA that issued the client certificates. If not present Dirk will use
# the standard CA certificates supplied with the server.
ca-cert: file:///home/me/dirk/security/certificates/ca.crt
# reload-threshold defines what duration before certificate expiry to being attempting to
# load new server certificates from server-cert and server-key URLs.
reload-threshold: '24h'
# reload-interval defines how often to attempt to reload certificates, once reload-threshold
# has been reached
reload-interval: '10m'
# storage-path is the path where information created by the slashing protection system is stored. If not
# supplied it will default to using the 'storage' directory in the user's home directory.
storage-path: /home/me/dirk/protection
Expand Down Expand Up @@ -121,6 +127,7 @@ Modules levels are used for each module, overriding the global log level. The a

- **accountmanager** operations on accounts such as locking and unlocking existing accounts, and generating new accounts
- **api** operations from the external API
- **certmanager** loads and reloads ssl certificates
- **checker** checks client access to operations
- **fetcher** fetches wallets and accounts from Ethereum 2 stores
- **lister** lists accounts that match a given path specification
Expand Down
65 changes: 36 additions & 29 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import (
standardrules "github.com/attestantio/dirk/rules/standard"
standardaccountmanager "github.com/attestantio/dirk/services/accountmanager/standard"
grpcapi "github.com/attestantio/dirk/services/api/grpc"
"github.com/attestantio/dirk/services/certmanager"
standardcertmanager "github.com/attestantio/dirk/services/certmanager/standard"
"github.com/attestantio/dirk/services/checker"
staticchecker "github.com/attestantio/dirk/services/checker/static"
"github.com/attestantio/dirk/services/fetcher"
Expand Down Expand Up @@ -206,6 +208,8 @@ func fetchConfig() (bool, error) {
viper.SetDefault("logging.timestamp.format", "2006-01-02T15:04:05.000Z07:00")
viper.SetDefault("storage-path", "storage")
viper.SetDefault("process.generation-timeout", 70*time.Second)
viper.SetDefault("certificates.reload-interval", 10*time.Minute)
viper.SetDefault("certificates.reload-threshold", 24*time.Hour)

if err := viper.ReadInConfig(); err != nil {
switch {
Expand Down Expand Up @@ -321,7 +325,13 @@ func startServices(ctx context.Context, majordomoSvc majordomo.Service, monitor
return errors.Wrap(err, "failed to set up ruler service")
}

_, err = startGrpcServer(ctx, monitor, majordomoSvc, stores, unlockerSvc, checkerSvc, fetcherSvc, rulerSvc)
// Set up the certmanager.
certManagerSvc, err := startCertManager(ctx, majordomoSvc)
if err != nil {
return errors.Wrap(err, "failed to set up certmanager service")
}

_, err = startGrpcServer(ctx, monitor, majordomoSvc, stores, unlockerSvc, checkerSvc, fetcherSvc, rulerSvc, certManagerSvc)
if err != nil {
return err
}
Expand Down Expand Up @@ -490,6 +500,17 @@ func startRuler(ctx context.Context, lockerSvc locker.Service, monitor metrics.S
)
}

func startCertManager(ctx context.Context, majordomoSvc majordomo.Service) (certmanager.Service, error) {
return standardcertmanager.New(ctx,
standardcertmanager.WithLogLevel(util.LogLevel("certmanager")),
standardcertmanager.WithMajordomo(majordomoSvc),
standardcertmanager.WithCertPEMURI(viper.GetString("certificates.server-cert")),
standardcertmanager.WithCertKeyURI(viper.GetString("certificates.server-key")),
standardcertmanager.WithReloadThreshold(viper.GetDuration("certificates.reload-threshold")),
standardcertmanager.WithReloadInterval(viper.GetDuration("certificates.reload-interval")),
)
}

func startPeers(ctx context.Context, monitor metrics.Service) (peers.Service, error) {
// Keys are strings.
peersInfo := viper.GetStringMapString("peers")
Expand Down Expand Up @@ -567,8 +588,7 @@ func startSigner(ctx context.Context,

func startSender(ctx context.Context,
monitor metrics.Service,
certPEMBlock []byte,
keyPEMBlock []byte,
certManagerSvc certmanager.Service,
caPEMBlock []byte,
) (
sender.Service,
Expand All @@ -583,8 +603,7 @@ func startSender(ctx context.Context,
sendergrpc.WithLogLevel(util.LogLevel("sender")),
sendergrpc.WithMonitor(senderMonitor),
sendergrpc.WithName(viper.GetString("server.name")),
sendergrpc.WithServerCert(certPEMBlock),
sendergrpc.WithServerKey(keyPEMBlock),
sendergrpc.WithCertManager(certManagerSvc),
sendergrpc.WithCACert(caPEMBlock),
)
if err != nil {
Expand All @@ -603,14 +622,13 @@ func startProcess(ctx context.Context,
checkerSvc checker.Service,
fetcherSvc fetcher.Service,
peersSvc peers.Service,
certPEMBlock []byte,
keyPEMBlock []byte,
certManagerSvc certmanager.Service,
caPEMBlock []byte,
) (
process.Service,
error,
) {
sender, err := startSender(ctx, monitor, certPEMBlock, keyPEMBlock, caPEMBlock)
sender, err := startSender(ctx, monitor, certManagerSvc, caPEMBlock)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -659,6 +677,7 @@ func startGrpcServer(ctx context.Context,
checkerSvc checker.Service,
fetcherSvc fetcher.Service,
rulerSvc ruler.Service,
certManagerSvc certmanager.Service,
) (
*grpcapi.Service,
error,
Expand All @@ -685,7 +704,7 @@ func startGrpcServer(ctx context.Context,
return nil, errors.Wrap(err, "failed to obtain server ID")
}

certPEMBlock, keyPEMBlock, caPEMBlock, err := obtainCerts(ctx, majordomoSvc)
caPEMBlock, err := obtainCA(ctx, majordomoSvc)
if err != nil {
return nil, err
}
Expand All @@ -699,8 +718,7 @@ func startGrpcServer(ctx context.Context,
checkerSvc,
fetcherSvc,
peersSvc,
certPEMBlock,
keyPEMBlock,
certManagerSvc,
caPEMBlock,
)
if err != nil {
Expand Down Expand Up @@ -755,8 +773,7 @@ func startGrpcServer(ctx context.Context,
grpcapi.WithPeers(peersSvc),
grpcapi.WithName(viper.GetString("server.name")),
grpcapi.WithID(serverID),
grpcapi.WithServerCert(certPEMBlock),
grpcapi.WithServerKey(keyPEMBlock),
grpcapi.WithCertManager(certManagerSvc),
grpcapi.WithCACert(caPEMBlock),
grpcapi.WithListenAddress(viper.GetString("server.listen-address")),
)
Expand All @@ -767,28 +784,18 @@ func startGrpcServer(ctx context.Context,
return svc, nil
}

func obtainCerts(ctx context.Context,
func obtainCA(ctx context.Context,
majordomoSvc majordomo.Service,
) (
[]byte,
[]byte,
[]byte,
error,
) {
certPEMBlock, err := majordomoSvc.Fetch(ctx, viper.GetString("certificates.server-cert"))
if err != nil {
return nil, nil, nil, errors.Wrap(err, fmt.Sprintf("failed to obtain server certificate from %s", viper.GetString("certificates.server-cert")))
if viper.GetString("certificates.ca-cert") == "" {
return nil, nil
Copy link
Contributor

@AntiD2ta AntiD2ta Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we return error here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

custom CAs are optional, I believe

}
keyPEMBlock, err := majordomoSvc.Fetch(ctx, viper.GetString("certificates.server-key"))
caPEMBlock, err := majordomoSvc.Fetch(ctx, viper.GetString("certificates.ca-cert"))
if err != nil {
return nil, nil, nil, errors.Wrap(err, fmt.Sprintf("failed to obtain server key from %s", viper.GetString("certificates.server-key")))
}
var caPEMBlock []byte
if viper.GetString("certificates.ca-cert") != "" {
caPEMBlock, err = majordomoSvc.Fetch(ctx, viper.GetString("certificates.ca-cert"))
if err != nil {
return nil, nil, nil, errors.Wrap(err, fmt.Sprintf("failed to obtain CA certificate from %s", viper.GetString("certificates.ca-cert")))
}
return nil, errors.Wrap(err, fmt.Sprintf("failed to obtain CA certificate from %s", viper.GetString("certificates.ca-cert")))
}
return certPEMBlock, keyPEMBlock, caPEMBlock, nil
return caPEMBlock, nil
}
41 changes: 25 additions & 16 deletions services/api/grpc/handlers/receiver/grpc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
mockrules "github.com/attestantio/dirk/rules/mock"
mockaccountmanager "github.com/attestantio/dirk/services/accountmanager/mock"
grpcapi "github.com/attestantio/dirk/services/api/grpc"
standardcertmanager "github.com/attestantio/dirk/services/certmanager/standard"
mockchecker "github.com/attestantio/dirk/services/checker/mock"
memfetcher "github.com/attestantio/dirk/services/fetcher/mem"
mocklister "github.com/attestantio/dirk/services/lister/mock"
Expand Down Expand Up @@ -300,14 +301,15 @@ func createServer(ctx context.Context, name string, id uint64, port uint32, base
}
mock.Processes[id] = process

certPEMBlock, err := os.ReadFile(filepath.Join(base, fmt.Sprintf("%s.crt", name)))
if err != nil {
return nil, errors.Wrap(err, "failed to obtain server certificate")
}
keyPEMBlock, err := os.ReadFile(filepath.Join(base, fmt.Sprintf("%s.key", name)))
if err != nil {
return nil, errors.Wrap(err, "failed to obtain server key")
}
certPEMURI := "file://" + filepath.Join(base, fmt.Sprintf("%s.crt", name))
certKeyURI := "file://" + filepath.Join(base, fmt.Sprintf("%s.key", name))

certManager, err := standardcertmanager.New(ctx,
standardcertmanager.WithLogLevel(zerolog.Disabled),
standardcertmanager.WithMajordomo(majordomo),
standardcertmanager.WithCertPEMURI(certPEMURI),
standardcertmanager.WithCertKeyURI(certKeyURI),
)
caPEMBlock, err := os.ReadFile(filepath.Join(base, "ca.crt"))
if err != nil {
return nil, errors.Wrap(err, "failed to obtain CA certificate")
Expand All @@ -317,8 +319,7 @@ func createServer(ctx context.Context, name string, id uint64, port uint32, base
grpcapi.WithLister(lister),
grpcapi.WithSigner(signer),
grpcapi.WithName(name),
grpcapi.WithServerCert(certPEMBlock),
grpcapi.WithServerKey(keyPEMBlock),
grpcapi.WithCertManager(certManager),
grpcapi.WithCACert(caPEMBlock),
grpcapi.WithPeers(peers),
grpcapi.WithID(id),
Expand All @@ -336,13 +337,22 @@ func createServer(ctx context.Context, name string, id uint64, port uint32, base
}

func createSender(ctx context.Context, name string, base string) (sender.Service, error) {
certPEMBlock, err := os.ReadFile(filepath.Join(base, fmt.Sprintf("%s.crt", name)))
majordomo, err := util.InitMajordomo(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain server certificate")
return nil, err
}
keyPEMBlock, err := os.ReadFile(filepath.Join(base, fmt.Sprintf("%s.key", name)))

certPEMURI := "file://" + filepath.Join(base, fmt.Sprintf("%s.crt", name))
certKeyURI := "file://" + filepath.Join(base, fmt.Sprintf("%s.key", name))

certManager, err := standardcertmanager.New(ctx,
standardcertmanager.WithLogLevel(zerolog.Disabled),
standardcertmanager.WithMajordomo(majordomo),
standardcertmanager.WithCertPEMURI(certPEMURI),
standardcertmanager.WithCertKeyURI(certKeyURI),
)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain server key")
return nil, errors.Wrap(err, "failed to create cert manager")
}
caPEMBlock, err := os.ReadFile(filepath.Join(base, "ca.crt"))
if err != nil {
Expand All @@ -351,8 +361,7 @@ func createSender(ctx context.Context, name string, base string) (sender.Service

return grpcsender.New(ctx,
grpcsender.WithName(name),
grpcsender.WithServerCert(certPEMBlock),
grpcsender.WithServerKey(keyPEMBlock),
grpcsender.WithCertManager(certManager),
grpcsender.WithCACert(caPEMBlock),
)
}
29 changes: 13 additions & 16 deletions services/api/grpc/parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package grpc

import (
"github.com/attestantio/dirk/services/accountmanager"
"github.com/attestantio/dirk/services/certmanager"
"github.com/attestantio/dirk/services/lister"
"github.com/attestantio/dirk/services/metrics"
"github.com/attestantio/dirk/services/peers"
Expand All @@ -34,11 +35,10 @@ type parameters struct {
walletManager walletmanager.Service
lister lister.Service
signer signer.Service
certManager certmanager.Service
name string
listenAddress string
id uint64
serverCert []byte
serverKey []byte
caCert []byte
}

Expand Down Expand Up @@ -130,17 +130,10 @@ func WithListenAddress(listenAddress string) Parameter {
})
}

// WithServerCert sets the server certificate for this module.
func WithServerCert(serverCert []byte) Parameter {
// WithCertManager sets the cert manager for this module.
func WithCertManager(service certmanager.Service) Parameter {
return parameterFunc(func(p *parameters) {
p.serverCert = serverCert
})
}

// WithServerKey sets the server key for this module.
func WithServerKey(serverKey []byte) Parameter {
return parameterFunc(func(p *parameters) {
p.serverKey = serverKey
p.certManager = service
})
}

Expand Down Expand Up @@ -193,11 +186,15 @@ func parseAndCheckParameters(params ...Parameter) (*parameters, error) {
if parameters.listenAddress == "" {
return nil, errors.New("no listen address specified")
}
if len(parameters.serverCert) == 0 {
return nil, errors.New("no server certificate specified")
if parameters.certManager == nil {
return nil, errors.New("no cert manager specified")
}
cert, err := parameters.certManager.GetCertificate(nil)
if err != nil {
return nil, errors.Wrap(err, "failed to get server certificate")
}
if len(parameters.serverKey) == 0 {
return nil, errors.New("no server key specified")
if len(cert.Certificate) == 0 {
return nil, errors.New("no server certificate specified")
}

return &parameters, nil
Expand Down
18 changes: 7 additions & 11 deletions services/api/grpc/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
signerhandler "github.com/attestantio/dirk/services/api/grpc/handlers/signer"
walletmanagerhandler "github.com/attestantio/dirk/services/api/grpc/handlers/walletmanager"
"github.com/attestantio/dirk/services/api/grpc/interceptors"
"github.com/attestantio/dirk/services/certmanager"
"github.com/attestantio/dirk/services/metrics"
"github.com/attestantio/dirk/util/loggers"
grpcmiddleware "github.com/grpc-ecosystem/go-grpc-middleware"
Expand Down Expand Up @@ -66,7 +67,7 @@ func New(ctx context.Context, params ...Parameter) (*Service, error) {
monitor: parameters.monitor,
}

if err := s.createServer(parameters.name, parameters.serverCert, parameters.serverKey, parameters.caCert); err != nil {
if err := s.createServer(parameters.name, parameters.certManager, parameters.caCert); err != nil {
return nil, errors.Wrap(err, "failed to create API server")
}

Expand Down Expand Up @@ -133,7 +134,7 @@ func New(ctx context.Context, params ...Parameter) (*Service, error) {
}

// createServer creates the GRPC server.
func (s *Service) createServer(name string, certPEMBlock []byte, keyPEMBlock []byte, caPEMBlock []byte) error {
func (s *Service) createServer(name string, certManager certmanager.Service, caPEMBlock []byte) error {
grpclog.SetLoggerV2(loggers.NewGRPCLoggerV2(log.With().Str("service", "grpc").Logger()))

grpcOpts := []grpc.ServerOption{
Expand All @@ -151,11 +152,6 @@ func (s *Service) createServer(name string, certPEMBlock []byte, keyPEMBlock []b
return errors.New("no server name provided; cannot proceed")
}

serverCert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)
if err != nil {
return errors.Wrap(err, "failed to load server keypair")
}

certPool := x509.NewCertPool()
if len(caPEMBlock) > 0 {
// Read in the certificate authority certificate; this is required to validate client certificates on incoming connections.
Expand All @@ -165,10 +161,10 @@ func (s *Service) createServer(name string, certPEMBlock []byte, keyPEMBlock []b
}

serverCreds := credentials.NewTLS(&tls.Config{
ClientAuth: tls.RequireAndVerifyClientCert,
Certificates: []tls.Certificate{serverCert},
ClientCAs: certPool,
MinVersion: tls.VersionTLS13,
ClientAuth: tls.RequireAndVerifyClientCert,
GetCertificate: certManager.GetCertificate,
ClientCAs: certPool,
MinVersion: tls.VersionTLS13,
})
grpcOpts = append(grpcOpts, grpc.Creds(serverCreds))
s.grpcServer = grpc.NewServer(grpcOpts...)
Expand Down
23 changes: 23 additions & 0 deletions services/certmanager/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright © 2025 Attestant Limited.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package certmanager

import "crypto/tls"

// Service is the tls certificate manager service.
type Service interface {
// GetCertificate gets the certificate, reloading it if configured to do so
// and necessary.
GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error)
}
Loading