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

[beatsauthextension] Add support for beats related ssl parameters to be used with otel-components #334

Merged
merged 14 commits into from
Feb 5, 2025
Prev Previous commit
Next Next commit
after manual testing
  • Loading branch information
khushijain21 committed Jan 29, 2025
commit 7a25f4edd1859a9a4e6c4f804f9d0bf40d59bd69
4 changes: 3 additions & 1 deletion distributions/elastic-components/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ extensions:
- gomod: github.com/open-telemetry/opentelemetry-collector-contrib/extension/pprofextension v0.117.0
- gomod: github.com/open-telemetry/opentelemetry-collector-contrib/extension/storage/filestorage v0.117.0
- gomod: go.opentelemetry.io/collector/extension/memorylimiterextension v0.117.0
- gomod: github.com/elastic/opentelemetry-collector-components/extension/beatsauthextension v0.0.0

connectors:
- gomod: github.com/elastic/opentelemetry-collector-components/connector/signaltometricsconnector v0.3.0
Expand Down Expand Up @@ -67,4 +68,5 @@ replaces:
- github.com/elastic/opentelemetry-collector-components/processor/lsmintervalprocessor => ../processor/lsmintervalprocessor
- github.com/elastic/opentelemetry-collector-components/connector/signaltometricsconnector => ../connector/signaltometricsconnector
- github.com/elastic/opentelemetry-collector-components/receiver/loadgenreceiver => ../receiver/loadgenreceiver
- github.com/elastic/opentelemetry-collector-components/processor/ratelimitprocessor => ../processor/ratelimitprocessor
- github.com/elastic/opentelemetry-collector-components/extension/beatsauthextension => ../extension/beatsauthextension

13 changes: 10 additions & 3 deletions extension/beatsauthextension/authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package beatsauthextension // import "github.com/elastic/opentelemetry-collector

import (
"context"
"fmt"
"net/http"

"github.com/elastic/elastic-agent-libs/transport/tlscommon"
Expand All @@ -42,7 +43,7 @@ func newAuthenticator(cfg *Config, telemetry component.TelemetrySettings) (*auth
func (a *authenticator) Start(ctx context.Context, host component.Host) error {
if a.cfg.TLS != nil {
tlsConfig, err := tlscommon.LoadTLSConfig(&tlscommon.Config{
VerificationMode: a.cfg.TLS.VerificationMode,
VerificationMode: tlsVerificationModes[a.cfg.TLS.VerificationMode],
CATrustedFingerprint: a.cfg.TLS.CATrustedFingerprint,
CASha256: a.cfg.TLS.CASha256,
})
Expand All @@ -62,7 +63,10 @@ func (a *authenticator) RoundTripper(base http.RoundTripper) (http.RoundTripper,
// At the time of writing, client.Transport is guaranteed to always have type *http.Transport.
// If this assumption is ever broken, we would need to create and use our own transport, and
// ignore the one passed in.
httpTransport := base.(*http.Transport)
httpTransport, ok := base.(*http.Transport)
if !ok {
return nil, fmt.Errorf("http.Roundripper is not of type *http.Transport")
}
if err := a.configureTransport(httpTransport); err != nil {
return nil, err
}
Expand All @@ -72,7 +76,10 @@ func (a *authenticator) RoundTripper(base http.RoundTripper) (http.RoundTripper,
func (a *authenticator) configureTransport(transport *http.Transport) error {
if a.tlsConfig != nil {
// injecting verifyConnection here, keeping all other fields on TLSConfig intact
transport.TLSClientConfig.VerifyConnection = a.tlsConfig.BuildModuleClientConfig(a.cfg.TLS.ServerName).VerifyConnection
beatTLSConfig := a.tlsConfig.BuildModuleClientConfig(a.tlsConfig.ServerName)
khushijain21 marked this conversation as resolved.
Show resolved Hide resolved

transport.TLSClientConfig.VerifyConnection = beatTLSConfig.VerifyConnection
transport.TLSClientConfig.InsecureSkipVerify = beatTLSConfig.InsecureSkipVerify
}
return nil
}
Expand Down
285 changes: 4 additions & 281 deletions extension/beatsauthextension/authenticator_test.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,10 @@
package beatsauthextension

import (
"bytes"
"context"
cryptorand "crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
"encoding/pem"
"fmt"
"math/big"
"math/rand"
"net"
"os"
"path/filepath"
"regexp"
"strconv"
"testing"
"time"

"github.com/stretchr/testify/require"
"github.com/tj/assert"
Expand All @@ -35,8 +19,8 @@ import (

// It tests whether VerifyConnection is set on tls.Config
func TestVerifyConnection(t *testing.T) {
testCerts := GenTestCerts(t)
fingerprint := GetFingerprint(testCerts["ca"])
testCerts := tlscommon.GenTestCerts(t)
fingerprint := tlscommon.GetCertFingerprint(testCerts["ca"])
settings := componenttest.NewNopTelemetrySettings()

httpClientConfig := confighttp.NewDefaultClientConfig()
Expand Down Expand Up @@ -112,7 +96,7 @@ func TestVerifyConnection(t *testing.T) {
t.Run(name, func(t *testing.T) {
cfg := &Config{
TLS: &TLSConfig{
VerificationMode: test.verificationMode,
VerificationMode: test.verificationMode.String(),
CATrustedFingerprint: test.CATrustedFingerprint,
CASha256: test.CASHA256,
},
Expand All @@ -134,7 +118,7 @@ func TestVerifyConnection(t *testing.T) {
// verifies if a callback was expected
verifier := auth.tlsConfig.ToConfig().VerifyConnection
if test.expectedCallback {
require.NotNil(t, verifier, "makeVerifyConnection returned a nil verifier")
require.NotNil(t, verifier, "VerifyConnection returned a nil verifier")
} else {
require.Nil(t, verifier)
return
Expand All @@ -159,264 +143,3 @@ type extensionsMap map[component.ID]component.Component
func (m extensionsMap) GetExtensions() map[component.ID]component.Component {
return m
}

func GenTestCerts(t *testing.T) map[string]*x509.Certificate {
ca, err := genCA()
if err != nil {
t.Fatalf("cannot generate root CA: %s", err)
}

unknownCA, err := genCA()
if err != nil {
t.Fatalf("cannot generate second root CA: %s", err)
}

certs := map[string]*x509.Certificate{
"ca": ca.Leaf,
}

certData := map[string]struct {
ca tls.Certificate
keyUsage x509.KeyUsage
isCA bool
dnsNames []string
ips []net.IP
expired bool
}{
"wildcard": {
ca: ca,
keyUsage: x509.KeyUsageDigitalSignature,
isCA: false,
dnsNames: []string{"*.example.com"},
},
"correct": {
ca: ca,
keyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
isCA: false,
dnsNames: []string{"localhost"},
// IPV4 and IPV6
ips: []net.IP{{127, 0, 0, 1}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
},
"unknown_authority": {
ca: unknownCA,
keyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
isCA: false,
dnsNames: []string{"localhost"},
// IPV4 and IPV6
ips: []net.IP{{127, 0, 0, 1}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
},
"expired": {
ca: ca,
keyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
isCA: false,
dnsNames: []string{"localhost"},
// IPV4 and IPV6
ips: []net.IP{{127, 0, 0, 1}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
expired: true,
},
}

tmpDir := t.TempDir()
for certName, data := range certData {
cert, err := genSignedCert(
data.ca,
data.keyUsage,
data.isCA,
certName,
data.dnsNames,
data.ips,
data.expired,
)
if err != nil {
t.Fatalf("could not generate certificate '%s': %s", certName, err)
}
certs[certName] = cert.Leaf

// We write the certificate to disk, so if the test fails the certs can
// be inspected/reused
certPEM := new(bytes.Buffer)
err = pem.Encode(certPEM, &pem.Block{
Type: "CERTIFICATE",
Bytes: cert.Leaf.Raw,
})
require.NoErrorf(t, err, "failed to encode certificste to PEM")

serverCertFile, err := os.Create(filepath.Join(tmpDir, certName+".crt"))
if err != nil {
t.Fatalf("creating file to write server certificate: %v", err)
}
if _, err := serverCertFile.Write(certPEM.Bytes()); err != nil {
t.Fatalf("writing server certificate: %v", err)
}

if err := serverCertFile.Close(); err != nil {
t.Fatalf("could not close certificate file: %s", err)
}
}

t.Cleanup(func() {
if t.Failed() {
finalDir := filepath.Join(os.TempDir(), cleanStr(t.Name())+strconv.Itoa(rand.Int()))
if err := os.Rename(tmpDir, finalDir); err != nil {
t.Fatalf("could not rename directory with certificates: %s", err)
}

t.Logf("certificates persisted on: '%s'", finalDir)
}
})

return certs
}

var cleanRegExp = regexp.MustCompile(`[^a-zA-Z0-9]`)

// cleanStr replaces all characters that do not match 'a-zA-Z0-9' by '_'
func cleanStr(path string) string {
return cleanRegExp.ReplaceAllString(path, "_")
}

// genSignedCert generates a CA and KeyPair and remove the need to depends on code of agent.
func genSignedCert(
ca tls.Certificate,
keyUsage x509.KeyUsage,
isCA bool,
commonName string,
dnsNames []string,
ips []net.IP,
expired bool,
) (tls.Certificate, error) {
if commonName == "" {
commonName = "You know, for search"
}

notBefore := time.Now()
notAfter := notBefore.Add(5 * time.Hour)

if expired {
notBefore = notBefore.Add(-42 * time.Hour)
notAfter = notAfter.Add(-42 * time.Hour)
}
// Create another Cert/key
cert := &x509.Certificate{
SerialNumber: big.NewInt(2000),

// SNA - Subject Alternative Name fields
IPAddresses: ips,
DNSNames: dnsNames,

Subject: pkix.Name{
CommonName: commonName,
Organization: []string{"TESTING"},
Country: []string{"CANADA"},
Province: []string{"QUEBEC"},
Locality: []string{"MONTREAL"},
StreetAddress: []string{"testing road"},
PostalCode: []string{"HOH OHO"},
},

NotBefore: notBefore,
NotAfter: notAfter,
IsCA: isCA,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: keyUsage,
BasicConstraintsValid: true,
}

certKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
if err != nil {
return tls.Certificate{}, fmt.Errorf("fail to generate RSA key: %w", err)
}

if isCA {
cert.SubjectKeyId = generateSubjectKeyID(&certKey.PublicKey)
}

certBytes, err := x509.CreateCertificate(
cryptorand.Reader,
cert,
ca.Leaf,
&certKey.PublicKey,
ca.PrivateKey,
)

if err != nil {
return tls.Certificate{}, fmt.Errorf("fail to create signed certificate: %w", err)
}

leaf, err := x509.ParseCertificate(certBytes)
if err != nil {
return tls.Certificate{}, fmt.Errorf("fail to parse the certificate: %w", err)
}

return tls.Certificate{
Certificate: [][]byte{certBytes},
PrivateKey: certKey,
Leaf: leaf,
}, nil
}

func generateSubjectKeyID(publicKey *rsa.PublicKey) []byte {
// SubjectKeyId generated using method 1 in RFC 7093, Section 2:
// 1) The keyIdentifier is composed of the leftmost 160-bits of the
// SHA-256 hash of the value of the BIT STRING subjectPublicKey
// (excluding the tag, length, and number of unused bits).
publicKeyBytes := x509.MarshalPKCS1PublicKey(publicKey)
h := sha256.Sum256(publicKeyBytes)
return h[:20]
}

func genCA() (tls.Certificate, error) {
ca := &x509.Certificate{
SerialNumber: serial(),
Subject: pkix.Name{
CommonName: "localhost",
Organization: []string{"TESTING"},
Country: []string{"CANADA"},
Province: []string{"QUEBEC"},
Locality: []string{"MONTREAL"},
StreetAddress: []string{"testing road"},
PostalCode: []string{"HOH OHO"},
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(1 * time.Hour),
IsCA: true,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
}

caKey, err := rsa.GenerateKey(cryptorand.Reader, 2048) // less secure key for quicker testing.
if err != nil {
return tls.Certificate{}, fmt.Errorf("fail to generate RSA key: %w", err)
}

ca.SubjectKeyId = generateSubjectKeyID(&caKey.PublicKey)

caBytes, err := x509.CreateCertificate(cryptorand.Reader, ca, ca, &caKey.PublicKey, caKey)
if err != nil {
return tls.Certificate{}, fmt.Errorf("fail to create certificate: %w", err)
}

leaf, err := x509.ParseCertificate(caBytes)
if err != nil {
return tls.Certificate{}, fmt.Errorf("fail to parse certificate: %w", err)
}

return tls.Certificate{
Certificate: [][]byte{caBytes},
PrivateKey: caKey,
Leaf: leaf,
}, nil
}

var ser int64 = 1

func serial() *big.Int {
ser = ser + 1
return big.NewInt(ser)
}

func GetFingerprint(cert *x509.Certificate) string {
caSHA256 := sha256.Sum256(cert.Raw)
return hex.EncodeToString(caSHA256[:])
}
Loading
Loading