Skip to content
Merged
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Changelog for NeoFS Node
- SN responds with server's API version only if it is older than client (#4110)
- SNs no longer sign TTL=1 requests over mutually authenticated inter-node connections (#4100)
- Optimized GRPC read/write bufferring (#4130)
- TLS key of SN is now always read from the node wallet instead of configuration (#4131)
Comment thread
roman-khimov marked this conversation as resolved.

### Removed
- Session token storage migration (#4124)
Expand All @@ -37,6 +38,8 @@ Metabase migrations for versions 8 and 9 introduced in NeoFS node 0.49.0
and 0.51.0 were removed from this release, upgrade to 0.55.0 first if using
earlier versions or resynchronize metabases.

Remove `grpc.tls.key` from the SN configuration: TLS keys are now always read from the node wallet.

## [0.55.0] - 2026-07-31 - Moido

### Added
Expand Down
1 change: 0 additions & 1 deletion cmd/neofs-adm/internal/modules/storagecfg/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ grpc:
tls:{{if .TLSCert}}
enabled: true # enable TLS for a gRPC connection (min version is TLS 1.2)
certificate: {{ .TLSCert }} # path to TLS certificate
key: {{ .TLSKey }} # path to TLS key
{{- else }}
enabled: false # disable TLS for a gRPC connection
{{- end}}
Expand Down
8 changes: 0 additions & 8 deletions cmd/neofs-adm/internal/modules/storagecfg/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ type config struct {
ControlEndpoint string
Endpoint string
TLSCert string
TLSKey string
MorphRPC []string
Attribute struct {
Locode string
Expand Down Expand Up @@ -233,13 +232,6 @@ func storageConfig(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
if c.TLSCert != "" {
c.TLSKey, err = getPath("TLS Key: ")
if err != nil {
return err
}
}

c.Relay, err = getConfirmation(false, "Use node as a relay? yes/[no]: ")
if err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion cmd/neofs-node/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ func initCfg(appCfg *config.Config) *cfg {
minConnTimeout := appCfg.APIClient.MinConnectionTime
pingInterval := appCfg.APIClient.PingInterval
pingTimeout := appCfg.APIClient.PingTimeout
getClientCertificate := clientCertificateProvider(appCfg.GRPC, key.PublicKey().Bytes())
getClientCertificate := clientCertificateProvider(appCfg.GRPC, &key.PrivateKey)
// Validate the certificate before serving requests so that it is trusted by peer SNs.
if getClientCertificate != nil {
_, err = getClientCertificate(nil)
Expand Down
1 change: 0 additions & 1 deletion cmd/neofs-node/config/grpc/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ func TestGRPCSection(t *testing.T) {

require.NotNil(t, tls)
require.Equal(t, "/path/to/cert", tls.Certificate)
require.Equal(t, "/path/to/key", tls.Key)
case 1:
require.Equal(t, "s02.neofs.devenv:8080", sc.Endpoint)
require.Equal(t, 0, sc.ConnLimit)
Expand Down
1 change: 0 additions & 1 deletion cmd/neofs-node/config/grpc/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ type GRPC struct {
type TLS struct {
Enabled bool `mapstructure:"enabled"`
Certificate string `mapstructure:"certificate"`
Key string `mapstructure:"key"`
}

// Normalize sets default values for GRPC configuration.
Expand Down
21 changes: 19 additions & 2 deletions cmd/neofs-node/config/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ grpc:
tls:
enabled: true
certificate: /path/to/cert
key: /path/to/key

- endpoint: s02.neofs.devenv:8080
conn_limit: -1
Expand All @@ -118,6 +117,25 @@ grpc:
`,
wantErr: false,
},
{
name: "unknown field grpc.tls.key",
config: `
grpc:
- endpoint: s01.neofs.devenv:8080
conn_limit: 1
tls:
enabled: true
certificate: /path/to/cert
key: /path/to/key

- endpoint: s02.neofs.devenv:8080
conn_limit: -1
tls:
enabled: false
- endpoint: s03.neofs.devenv:8080
`,
wantErr: true,
},
{
name: "unknown field grpc.key",
config: `
Expand Down Expand Up @@ -146,7 +164,6 @@ grpc:
tls:
enabled: true
certificate: /path/to/cert
key: /path/to/key

- endpoint: s02.neofs.devenv:8080
conn_limit: -1
Expand Down
23 changes: 8 additions & 15 deletions cmd/neofs-node/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ func (s grpcServerSnapshot) unchanged(other grpcServerSnapshot) bool {
return s.ConnLimit == other.ConnLimit &&
s.TLS.Enabled == other.TLS.Enabled &&
s.TLS.Certificate == other.TLS.Certificate &&
s.TLS.Key == other.TLS.Key &&
s.certFingerprint == other.certFingerprint
}

Expand All @@ -45,25 +44,19 @@ func writeGRPCConfig(c *config.Config) grpcConfigSnapshot {
for i, sc := range c.GRPC {
snap[i] = grpcServerSnapshot{
GRPC: sc,
certFingerprint: tlsCertFingerprint(sc.TLS.Certificate, sc.TLS.Key),
certFingerprint: tlsCertFingerprint(sc.TLS.Certificate),
}
}
return snap
}

func tlsCertFingerprint(certFile, keyFile string) string {
if keyFile == "" {
func tlsCertFingerprint(certFile string) string {
if certFile == "" {
return ""
}

h := sha256.New()
err := hashFileLimited(h, certFile, maxTLSFingerprintFileBytes)
if err != nil {
return ""
}
_, _ = h.Write([]byte{0})
err = hashFileLimited(h, keyFile, maxTLSFingerprintFileBytes)
if err != nil {
if err := hashFileLimited(h, certFile, maxTLSFingerprintFileBytes); err != nil {
return ""
}

Expand Down Expand Up @@ -188,18 +181,18 @@ func buildSingleGRPCServer(c *cfg, sc grpcconfig.GRPC, maxRecvMsgSizeOpt grpc.Se

tlsCfg := sc.TLS

if tlsCfg.Key != "" {
certFile, keyFile := tlsCfg.Certificate, tlsCfg.Key
if tlsCfg.Enabled {
certFile := tlsCfg.Certificate

if _, err := tls.LoadX509KeyPair(certFile, keyFile); err != nil {
if _, err := loadTLSCertificate(certFile, &c.key.PrivateKey); err != nil {
c.log.Error("could not read certificate from file", zap.Error(err))
return nil, nil, err
}

// read certificate from disk on each handshake to pick up renewals automatically.
creds := trustedPeerTLSCredentials(&tls.Config{
GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
cert, err := loadTLSCertificate(certFile, &c.key.PrivateKey)
if err != nil {
return nil, fmt.Errorf("reload TLS certificate: %w", err)
}
Expand Down
51 changes: 37 additions & 14 deletions cmd/neofs-node/mtls.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package main

import (
"bytes"
"crypto/ecdsa"
"crypto/tls"
"errors"
"crypto/x509"
"encoding/pem"
"fmt"
"io"
"net"
"os"

grpcconfig "github.com/nspcc-dev/neofs-node/cmd/neofs-node/config/grpc"
"github.com/nspcc-dev/neofs-node/pkg/network/peerauth"
Expand Down Expand Up @@ -41,35 +44,55 @@ func (x trustedPeerCredentials) ServerHandshake(conn net.Conn) (net.Conn, creden
return conn, trustedInfo, nil
}

func clientCertificateProvider(cfgs []grpcconfig.GRPC, expectedPublicKey []byte) func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
func clientCertificateProvider(cfgs []grpcconfig.GRPC, key *ecdsa.PrivateKey) func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
for i := range cfgs {
if !cfgs[i].TLS.Enabled {
continue
}

certFile, keyFile := cfgs[i].TLS.Certificate, cfgs[i].TLS.Key
certFile := cfgs[i].TLS.Certificate
return func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
cert, err := loadTLSCertificate(certFile, key)
if err != nil {
return nil, fmt.Errorf("reload TLS client certificate: %w", err)
}
if err := verifyTLSCertificatePublicKey(&cert, expectedPublicKey); err != nil {
return nil, err
}
return &cert, nil
}
}

return nil
}

func verifyTLSCertificatePublicKey(cert *tls.Certificate, expected []byte) error {
pub, err := peerauth.CertificatePublicKeyFromRaw(cert.Certificate)
const maxTLSCertificateFileBytes = 16 << 10 // 16 KB

// loadTLSCertificate reads a PEM-encoded certificate chain and pairs it with
// the node's private key. The leaf certificate must be issued for the node key.
func loadTLSCertificate(certFile string, key *ecdsa.PrivateKey) (tls.Certificate, error) {
certPEM, err := readTLSCertificateFile(certFile)
if err != nil {
return fmt.Errorf("parse TLS client certificate public key: %w", err)
return tls.Certificate{}, err
}
if !bytes.Equal(pub, expected) {
return errors.New("TLS client certificate public key differs from node public key")
keyDER, err := x509.MarshalECPrivateKey(key)
if err != nil {
return tls.Certificate{}, fmt.Errorf("marshal node TLS private key: %w", err)
}
return nil

return tls.X509KeyPair(certPEM, pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}))
}

func readTLSCertificateFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer func() { _ = f.Close() }()

data, err := io.ReadAll(io.LimitReader(f, maxTLSCertificateFileBytes+1))
if err != nil {
return nil, err
}
if len(data) > maxTLSCertificateFileBytes {
return nil, fmt.Errorf("TLS certificate file exceeds %d bytes", maxTLSCertificateFileBytes)
}
return data, nil
}
Loading
Loading