Skip to content

Commit

Permalink
TLS client authentication (#28)
Browse files Browse the repository at this point in the history
* Implement TLS client authentication.

* Update readme.

* Improve error messages, terminate with new line.

* Print error if only one of client cert/key were set.
  • Loading branch information
icedream authored Mar 24, 2023
1 parent 36933ad commit 585dc66
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 3 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,10 @@ Usage: gobgp-exporter [arguments]
Whether to enable TLS for gRPC API access.
-gobgp.tls-ca string
Optional path to PEM file with CA certificates to be trusted for gRPC API access.
-gobgp.tls-client-cert string
Optional path to PEM file with client certificate to be used for client authentication.
-gobgp.tls-client-key string
Optional path to PEM file with client key to be used for client authentication.
-gobgp.tls-server-name string
Optional hostname to verify API server as.
-log.level string
Expand All @@ -298,6 +302,8 @@ Documentation: https://github.com/greenpau/gobgp_exporter/
instance), or the address of a remote GoBGP server.
* __`gobgp.tls`:__ Enable TLS for the GoBGP connection. (default: false)
* __`gobgp.tls-ca`:__ Optional path to a PEM file containing certificate authorities to verify GoBGP server certificate against. If empty, the host's root CA set is used instead. (default: empty)
* __`gobgp.tls-client-cert`:__ Optional path to a PEM file containing the client certificate to authenticate with. (default: empty)
* __`gobgp.tls-client-key`:__ Optional path to a PEM file containing the key for theclient certificate to authenticate with. (default: empty)
* __`gobgp.tls-server-name`:__ Optional server name to verify GoBGP server certificate against. If empty, verification will be using the hostname or IP used in `gobgp.address`. (default: empty)
* __`gobgp.timeout`:__ Timeout on gRPC requests to GoBGP.
* __`gobgp.poll-interval`:__ The minimum interval (in seconds) between collections from GoBGP server. (default: 15 seconds)
Expand Down
106 changes: 103 additions & 3 deletions cmd/gobgp_exporter/main.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package main

import (
"crypto"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
"flag"
"fmt"
"net/http"
Expand All @@ -14,13 +17,88 @@ import (
"github.com/prometheus/common/promlog"
)

func loadCertificatePEM(filePath string) (*x509.Certificate, error) {
content, err := os.ReadFile(filePath)
if err != nil {
return nil, err
}

rest := content
var block *pem.Block
var cert *x509.Certificate
for len(rest) > 0 {
block, rest = pem.Decode(content)
if block == nil {
// no PEM data found, rest will not have been modified
break
}
content = rest
switch block.Type {
case "CERTIFICATE":
cert, err = x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, err
}
return cert, err
default:
// not the PEM block we're looking for
continue
}
}
return nil, errors.New("no certificate PEM block found")
}

func loadKeyPEM(filePath string) (crypto.PrivateKey, error) {
content, err := os.ReadFile(filePath)
if err != nil {
return nil, err
}

rest := content
var block *pem.Block
var key crypto.PrivateKey
for len(rest) > 0 {
block, rest = pem.Decode(content)
if block == nil {
// no PEM data found, rest will not have been modified
break
}
switch block.Type {
case "RSA PRIVATE KEY":
key, err = x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
return key, err
case "PRIVATE KEY":
key, err = x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
return key, err
case "EC PRIVATE KEY":
key, err = x509.ParseECPrivateKey(block.Bytes)
if err != nil {
return nil, err
}
return key, err
default:
// not the PEM block we're looking for
continue
}
}
return nil, errors.New("no private key PEM block found")
}

func main() {
var listenAddress string
var metricsPath string
var serverAddress string
var serverTLS bool
var serverTLSCAPath string
var serverTLSServerName string
var serverTLSClientCertPath string
var serverTLSClientKeyPath string
var pollTimeout int
var pollInterval int
var isShowMetrics bool
Expand All @@ -34,6 +112,8 @@ func main() {
flag.BoolVar(&serverTLS, "gobgp.tls", false, "Whether to enable TLS for gRPC API access.")
flag.StringVar(&serverTLSCAPath, "gobgp.tls-ca", "", "Optional path to PEM file with CA certificates to be trusted for gRPC API access.")
flag.StringVar(&serverTLSServerName, "gobgp.tls-server-name", "", "Optional hostname to verify API server as.")
flag.StringVar(&serverTLSClientCertPath, "gobgp.tls-client-cert", "", "Optional path to PEM file with client certificate to be used for client authentication.")
flag.StringVar(&serverTLSClientKeyPath, "gobgp.tls-client-key", "", "Optional path to PEM file with client key to be used for client authentication.")
flag.IntVar(&pollTimeout, "gobgp.timeout", 2, "Timeout on gRPC requests to a GoBGP server.")
flag.IntVar(&pollInterval, "gobgp.poll-interval", 15, "The minimum interval (in seconds) between collections from a GoBGP server.")
flag.StringVar(&authToken, "auth.token", "anonymous", "The X-Token for accessing the exporter itself")
Expand All @@ -57,7 +137,7 @@ func main() {

allowedLogLevel := &promlog.AllowedLevel{}
if err := allowedLogLevel.Set(logLevel); err != nil {
fmt.Fprintf(os.Stderr, "%s", err.Error())
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
os.Exit(1)
}

Expand All @@ -74,20 +154,40 @@ func main() {
// assuming PEM file here
pemCerts, err := os.ReadFile(filepath.Clean(serverTLSCAPath))
if err != nil {
fmt.Fprintf(os.Stderr, "Could not read TLS CA PEM file %q: %s", serverTLSCAPath, err)
fmt.Fprintf(os.Stderr, "Could not read TLS CA PEM file %q: %s\n", serverTLSCAPath, err)
os.Exit(1)
}

opts.TLS.RootCAs = x509.NewCertPool()
ok := opts.TLS.RootCAs.AppendCertsFromPEM(pemCerts)
if !ok {
fmt.Fprintf(os.Stderr, "Could not parse any TLS CA certificate from PEM file %q: %s", serverTLSCAPath, err)
fmt.Fprintf(os.Stderr, "Could not parse any TLS CA certificate from PEM file %q: %s\n", serverTLSCAPath, err)
os.Exit(1)
}
}
if len(serverTLSServerName) > 0 {
opts.TLS.ServerName = serverTLSServerName
}
if len(serverTLSClientCertPath) > 0 && len(serverTLSClientKeyPath) > 0 {
// again assuming PEM file
cert, err := loadCertificatePEM(serverTLSClientCertPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to load client certificate: %s\n", err)
}
key, err := loadKeyPEM(serverTLSClientKeyPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to load client key: %s\n", err)
}
opts.TLS.Certificates = []tls.Certificate{
{
Certificate: [][]byte{cert.Raw},
PrivateKey: key,
},
}
} else if len(serverTLSClientCertPath) > 0 || len(serverTLSClientKeyPath) > 0 {
fmt.Fprintln(os.Stderr, "Only one of client certificate and key was set, must set both.")
os.Exit(1)
}
}

if isShowVersion {
Expand Down

0 comments on commit 585dc66

Please sign in to comment.