Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable mTLS support for gNMI exporter. #15

Merged
merged 11 commits into from
Oct 7, 2024
16 changes: 15 additions & 1 deletion cmd/clio.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ package main

import (
"log"
"path/filepath"

"github.com/openconfig/clio/collector"
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/confmap"
"go.opentelemetry.io/collector/confmap/provider/fileprovider"
"go.opentelemetry.io/collector/otelcol"
)

Expand All @@ -30,7 +33,18 @@ func main() {
Version: "1.0.0",
}

if err := runInteractive(otelcol.CollectorSettings{BuildInfo: info, Factories: collector.Components}); err != nil {
settings := otelcol.CollectorSettings{
BuildInfo: info,
Factories: collector.Components,
ConfigProviderSettings: otelcol.ConfigProviderSettings{
ResolverSettings: confmap.ResolverSettings{
URIs: []string{filepath.Join("../config", "config.yaml")},
ProviderFactories: []confmap.ProviderFactory{fileprovider.NewFactory()},
DefaultScheme: "file",
},
},
}
if err := runInteractive(settings); err != nil {
log.Fatal(err)
}
}
Expand Down
3 changes: 3 additions & 0 deletions gnmi/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ type Config struct {
// Addr is the listen address of the gNMI server.
Addr string `mapstructure:"addr"`

// CaFile is the CA certificate to use for mTLS.
CaFile string `mapstructure:"ca_file"`
LarsxGitHub marked this conversation as resolved.
Show resolved Hide resolved

// CertFile is the certificate to use for TLS.
CertFile string `mapstructure:"cert_file"`

Expand Down
5 changes: 2 additions & 3 deletions gnmi/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,14 @@ import (

"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/confmap"
"go.opentelemetry.io/collector/confmap/confmaptest"
)

func TestUnmarshalDefaultConfig(t *testing.T) {
factory := NewFactory()
cfg := factory.CreateDefaultConfig()
if err := component.UnmarshalConfig(confmap.New(), cfg); err != nil {
if err := confmap.New().Unmarshal(cfg); err != nil {
t.Errorf("UnmarshalConfig returned error: %v", err)
}

Expand All @@ -42,7 +41,7 @@ func TestUnmarshalConfig(t *testing.T) {
require.NoError(t, err)
factory := NewFactory()
got := factory.CreateDefaultConfig()
if err := component.UnmarshalConfig(cm, got); err != nil {
if err := cm.Unmarshal(got); err != nil {
t.Errorf("UnmarshalConfig returned error: %v", err)
}

Expand Down
12 changes: 4 additions & 8 deletions gnmi/gnmi.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import (
"go.opentelemetry.io/collector/pdata/pmetric"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/reflection"
"k8s.io/klog/v2"
)
Expand Down Expand Up @@ -60,14 +59,11 @@ func NewGNMIExporter(logger *zap.Logger, cfg *Config) (*GNMI, error) {
}

var opts []grpc.ServerOption

if cfg.CertFile != "" {
creds, err := credentials.NewServerTLSFromFile(cfg.CertFile, cfg.KeyFile)
if err != nil {
return nil, fmt.Errorf("cannot create gNMI credentials, %v", err)
}
opts = append(opts, grpc.Creds(creds))
opt, err := gRPCSecurityOption(cfg)
if err != nil {
return nil, err
}
opts = append(opts, opt...)

return &GNMI{
cfg: cfg,
Expand Down
12 changes: 8 additions & 4 deletions gnmi/gnmi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,13 @@ func TestHandleMetrics(t *testing.T) {
},
metricCh: make(chan *pmetric.Metrics, 10),
}
var n *gpb.Notification

var cnt int
updateFn := func(notif *gpb.Notification) error {
n = notif
cnt++
LarsxGitHub marked this conversation as resolved.
Show resolved Hide resolved
for _, update := range notif.GetUpdate() {
cnt += int(update.GetDuplicates())
}
return nil
}

Expand All @@ -85,8 +89,8 @@ func TestHandleMetrics(t *testing.T) {
close(g.metricCh)

time.Sleep(time.Second)
if len(n.Update) != tc.wantCnt {
t.Errorf("missing updates: want %d got %d", tc.wantCnt, len(n.Update))
if cnt != tc.wantCnt {
t.Errorf("missing updates: want %d got %d", tc.wantCnt, cnt)
}
})
}
Expand Down
107 changes: 107 additions & 0 deletions gnmi/security.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright 2024 Google LLC
//
// 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 gnmi

import (
"errors"
"fmt"
"os"
"time"

"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/tls/certprovider/pemfile"
"google.golang.org/grpc/security/advancedtls"
)

const (
credsRefreshDuration = 24 * time.Hour
LarsxGitHub marked this conversation as resolved.
Show resolved Hide resolved
)

func gRPCSecurityOption(cfg *Config) ([]grpc.ServerOption, error) {
LarsxGitHub marked this conversation as resolved.
Show resolved Hide resolved
var opts []grpc.ServerOption
var err error

switch {
case cfg.CaFile != "":
opts, err = optionMutualTLS(cfg)
case cfg.CaFile == "" && cfg.CertFile != "":
opts, err = optionTLS(cfg)
}
if err != nil {
return nil, fmt.Errorf("failed to create gNMI credentials: %v", err)
}
return opts, nil
}

func optionTLS(cfg *Config) ([]grpc.ServerOption, error) {
creds, err := credentials.NewServerTLSFromFile(cfg.CertFile, cfg.KeyFile)
if err != nil {
return nil, err
}
return []grpc.ServerOption{grpc.Creds(creds)}, nil
}

func optionMutualTLS(cfg *Config) ([]grpc.ServerOption, error) {

// Check that all needed files actually exist.
for _, f := range []string{cfg.CertFile, cfg.KeyFile, cfg.CaFile} {
if _, err := os.Stat(f); errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("file %q does not exist", f)
}
}

// Get a provider for the identity credentials.
identity := pemfile.Options{
CertFile: cfg.CertFile,
KeyFile: cfg.KeyFile,
RefreshDuration: credsRefreshDuration,
}

identityProvider, err := pemfile.NewProvider(identity)
if err != nil {
return nil, fmt.Errorf("failed to create identity provider: %v", err)
}

// Get a provider for the root credentials.
root := pemfile.Options{
RootFile: cfg.CaFile,
RefreshDuration: credsRefreshDuration,
}
rootProvider, err := pemfile.NewProvider(root)
if err != nil {
return nil, fmt.Errorf("failed to create root provider: %v", err)
}

// Setup the mTLS option.
options := &advancedtls.Options{
IdentityOptions: advancedtls.IdentityCertificateOptions{
IdentityProvider: identityProvider,
},
RootOptions: advancedtls.RootCertificateOptions{
RootProvider: rootProvider,
},
RequireClientCert: true,
}

// Setup the server credentials.
creds, err := advancedtls.NewServerCreds(options)
if err != nil {
return nil, fmt.Errorf("failed to create client creds: %v", err)
}

return []grpc.ServerOption{grpc.Creds(creds)}, nil

}
129 changes: 129 additions & 0 deletions gnmi/security_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Copyright 2024 Google LLC
//
// 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 gnmi

import (
"testing"
)

var (
certFile = "testdata/cert.pem"
keyFile = "testdata/private.pem"
caFile = "testdata/gnmi.example.com.crt"
)

func TestGRPCSecurityOption(t *testing.T) {
tests := []struct {
name string
cfg *Config
wantCnt int
}{
{
name: "no-tls",
cfg: &Config{},
},
{
name: "tls",
cfg: &Config{
CertFile: certFile,
KeyFile: keyFile,
},
wantCnt: 1,
},
{
name: "mtls",
cfg: &Config{
CaFile: caFile,
CertFile: certFile,
KeyFile: keyFile,
},
wantCnt: 1,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := gRPCSecurityOption(tc.cfg)
if err != nil {
t.Errorf("gRPCSecurityOption returned error: %v", err)
}
if len(got) != tc.wantCnt {
t.Errorf("gRPCSecurityOption returned %d options, want %d", len(got), tc.wantCnt)
}
})
}
}

func TestGRPCSecurityOptionErrors(t *testing.T) {
tests := []struct {
name string
cfg *Config
errMsg string
}{
{
name: "tls-nonexist-cert",
cfg: &Config{
CertFile: "testdata/capybara-stole-this-cert.pem",
KeyFile: keyFile,
},
errMsg: "for nonexistent client certificate",
},
{
name: "tls-nonexist-key",
cfg: &Config{
CertFile: certFile,
KeyFile: "testdata/capybara-stole-this-key.pem",
},
errMsg: "for nonexistent client private key",
},
{
name: "mtls-nonexist-ca-cert",
cfg: &Config{
CaFile: "testdata/capybara-stole-this-ca-cert.crt",
CertFile: certFile,
KeyFile: keyFile,
},
errMsg: "for nonexistent CA certificate",
},
{
name: "mtls-nonexist-cli-cert",
cfg: &Config{
CaFile: caFile,
CertFile: "testdata/capybara-stole-this-cert.pem",
KeyFile: keyFile,
},
errMsg: "for nonexistent client certificate",
},
{
name: "mtls-nonexist-key",
cfg: &Config{
CaFile: caFile,
CertFile: certFile,
KeyFile: "testdata/capybara-stole-this-key.pem",
},
errMsg: "for nonexistent client private key",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
_, err := gRPCSecurityOption(tc.cfg)
if err == nil {
t.Errorf("gRPCSecurityOption did not return error %v", tc.errMsg)
}

})
}
}
26 changes: 26 additions & 0 deletions gnmi/testdata/cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
-----BEGIN CERTIFICATE-----
MIIEXzCCAkcCFGvSOmNP0qOPUCg8CLz8h+lmkBPEMA0GCSqGSIb3DQEBCwUAMGwx
GTAXBgNVBAMMEGdubWkuZXhhbXBsZS5jb20xDzANBgNVBAsMBmdpdGh1YjENMAsG
A1UECgwEY2xpbzERMA8GA1UEBwwIRHVibGluIDQxDzANBgNVBAgMBkR1YmxpbjEL
MAkGA1UEBhMCSUUwHhcNMjQwNzE5MTYzMDU1WhcNMjUwNzE5MTYzMDU1WjBsMRkw
FwYDVQQDDBBnbm1pLmV4YW1wbGUuY29tMQ8wDQYDVQQLDAZnaXRodWIxDTALBgNV
BAoMBGNsaW8xETAPBgNVBAcMCER1YmxpbiA0MQ8wDQYDVQQIDAZEdWJsaW4xCzAJ
BgNVBAYTAklFMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxzXqfr6O
xO9gm+HcjNxF/b7nq5ij/OGySoJeqFw6wqSUhRRzoWBCirx2O1i8qVkPJ8dktKD1
mCxsgeIppCopvEBnzIvexPLEpo8aASj4qKTMNys6TtcK4Cbw6h2mytn3LVlYl49D
Pg4f7TCYz3RMoKXEh33dAOTfuYi0uV/dalPo2vCPlId5zZ8Uraa+EKhPx8UfNlBl
AmRSMq9Hqf3efeQFZPHVsVOzkUFMi66z9cdg87vy8BrZ+xvVY0Z/3eGQbvUZTyKB
inG8rYOvGElRidB5hZ7h7Nqq90ozHblR8MjGv3PZGcp1qokrLdpQAIDm3LuOWrlU
sIG6t82Y2HVHwQIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQAwrEPPvfpXRekI/NPq
/pipaLCcBaT2PVR2F1Q0gSDQIDEXuQ3Z8GjmsIFxUQ4Jd5huYmUmUZx8LBA+b/fN
bJHcYyNSz5vIBEX3B/EDHkUVF6SnlO/4i6y0U2VEvEX2iqb8+MvSj6XDYNm5d7oE
bEbniPloGtG6uJqWrTv43vZvhOOOh26HsRbMWXEZTbZAFJGzF5LWgWJWwZOh3rdt
7SkoMhW2axiG7DaAuNuy2dwEhyxGYaskvW+X7uNT3EJcBcxqKzXInP9luuHpuTJg
gF55GB5uXZAVZWiBja9ajMdJ1ezmlrT6yXPyIsQBG0eXu6YNTS0w824qtOcp6xRL
BMriq/y65gnxh+aDyPWMN0GWp3ZuitjXfa/htDAnI5lF78kPJvErF/mJ3/v86eX6
FQKXb8D3kFGMYcrBrFm77v+GeM3U5s+4bIU9OKPtgNqbpiFpogKVWoUnewPpDq42
T3HCw1SRpSEF/4EqZ92Ka8uJlURXL8DK7Be8KvnWDfsxEhmlIlw+cYrLtAEzrkbn
Vb/g/JTVzYqVk55bnjmf5e1MZU6KBmT2j/nvNymAi40gG2t/ylFcJyJWSV9uUg9k
K29lRZ3ntW5gz5L4sdcC+vj8BDJzrJRYtPitGrtXrXfJbseBgrVu7BS4m0qNoM5C
4jWrVUQGNPcUyWCagqTZAd1C7w==
-----END CERTIFICATE-----
Loading