Skip to content

Commit f89e3a2

Browse files
authored
Merge pull request #1175 from nginx/add-ssl-support-v3
Add support for ssl in v3
2 parents 66d9d31 + f009a61 commit f89e3a2

File tree

22 files changed

+390
-12
lines changed

22 files changed

+390
-12
lines changed

api/grpc/mpi/v1/command.pb.go

Lines changed: 13 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/grpc/mpi/v1/command.pb.validate.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/grpc/mpi/v1/command.proto

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,8 @@ message APIDetails {
352352
string location = 1;
353353
// the API listen directive
354354
string listen = 2;
355+
// the API CA file path
356+
string Ca = 3;
355357
}
356358

357359
// A set of runtime NGINX App Protect settings

docs/proto/protos.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,7 @@ Perform an associated API action on an instance
678678
| ----- | ---- | ----- | ----------- |
679679
| location | [string](#string) | | the API location directive |
680680
| listen | [string](#string) | | the API listen directive |
681+
| Ca | [string](#string) | | the API CA file path |
681682

682683

683684

internal/collector/nginxossreceiver/internal/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type APIDetails struct {
3333
URL string `mapstructure:"url"`
3434
Listen string `mapstructure:"listen"`
3535
Location string `mapstructure:"location"`
36+
Ca string `mapstructure:"ca"`
3637
}
3738

3839
type AccessLog struct {
@@ -56,6 +57,7 @@ func CreateDefaultConfig() component.Config {
5657
URL: "http://localhost:80/status",
5758
Listen: "localhost:80",
5859
Location: "status",
60+
Ca: "",
5961
},
6062
}
6163
}

internal/collector/nginxossreceiver/internal/scraper/stubstatus/stub_status_scraper.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ package stubstatus
77

88
import (
99
"context"
10+
"crypto/tls"
11+
"crypto/x509"
1012
"net"
1113
"net/http"
14+
"os"
1215
"strings"
1316
"sync"
1417
"time"
@@ -63,6 +66,28 @@ func (s *NginxStubStatusScraper) ID() component.ID {
6366
func (s *NginxStubStatusScraper) Start(_ context.Context, _ component.Host) error {
6467
s.logger.Info("Starting NGINX stub status scraper")
6568
httpClient := http.DefaultClient
69+
caCertLocation := s.cfg.APIDetails.Ca
70+
if caCertLocation != "" {
71+
s.settings.Logger.Debug("Reading CA certificate", zap.Any("file_path", caCertLocation))
72+
caCert, err := os.ReadFile(caCertLocation)
73+
if err != nil {
74+
s.settings.Logger.Error("Error starting NGINX stub status scraper. "+
75+
"Failed to read CA certificate", zap.Error(err))
76+
77+
return nil
78+
}
79+
caCertPool := x509.NewCertPool()
80+
caCertPool.AppendCertsFromPEM(caCert)
81+
82+
httpClient = &http.Client{
83+
Transport: &http.Transport{
84+
TLSClientConfig: &tls.Config{
85+
RootCAs: caCertPool,
86+
MinVersion: tls.VersionTLS13,
87+
},
88+
},
89+
}
90+
}
6691
httpClient.Timeout = s.cfg.ClientConfig.Timeout
6792

6893
if strings.HasPrefix(s.cfg.APIDetails.Listen, "unix:") {
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// Copyright (c) F5, Inc.
2+
//
3+
// This source code is licensed under the Apache License, Version 2.0 license found in the
4+
// LICENSE file in the root directory of this source tree.
5+
6+
package stubstatus
7+
8+
import (
9+
"context"
10+
"crypto/tls"
11+
"crypto/x509"
12+
"net"
13+
"net/http"
14+
"net/http/httptest"
15+
"os"
16+
"testing"
17+
18+
"github.com/stretchr/testify/assert"
19+
"github.com/stretchr/testify/require"
20+
"go.opentelemetry.io/collector/component"
21+
"go.opentelemetry.io/collector/component/componenttest"
22+
"go.opentelemetry.io/collector/receiver/receivertest"
23+
24+
"github.com/nginx/agent/v3/internal/collector/nginxossreceiver/internal/config"
25+
"github.com/nginx/agent/v3/test/helpers"
26+
)
27+
28+
func TestStubStatusScraperTLS(t *testing.T) {
29+
// Generate self-signed certificate using helper
30+
keyBytes, certBytes := helpers.GenerateSelfSignedCert(t)
31+
32+
// Create a temporary directory for test files
33+
tempDir := t.TempDir()
34+
35+
// Save certificate to a file
36+
certFile := helpers.WriteCertFiles(t, tempDir, helpers.Cert{
37+
Name: "server.crt",
38+
Type: "CERTIFICATE",
39+
Contents: certBytes,
40+
})
41+
42+
// Parse the private key
43+
key, err := x509.ParsePKCS1PrivateKey(keyBytes)
44+
require.NoError(t, err)
45+
46+
// Create a TLS config with our self-signed certificate
47+
tlsCert := tls.Certificate{
48+
Certificate: [][]byte{certBytes},
49+
PrivateKey: key,
50+
}
51+
52+
serverTLSConfig := &tls.Config{
53+
MinVersion: tls.VersionTLS13,
54+
Certificates: []tls.Certificate{tlsCert},
55+
}
56+
57+
// Create a test server with our custom TLS config
58+
server := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
59+
if req.URL.Path == "/status" {
60+
rw.WriteHeader(http.StatusOK)
61+
_, _ = rw.Write([]byte(`Active connections: 291
62+
server accepts handled requests
63+
16630948 16630946 31070465
64+
Reading: 6 Writing: 179 Waiting: 106
65+
`))
66+
67+
return
68+
}
69+
rw.WriteHeader(http.StatusNotFound)
70+
}))
71+
72+
server.TLS = serverTLSConfig
73+
server.StartTLS()
74+
defer server.Close()
75+
76+
// Test with TLS configuration using our self-signed certificate
77+
t.Run("Test 1: self-signed TLS", func(t *testing.T) {
78+
cfg, ok := config.CreateDefaultConfig().(*config.Config)
79+
require.True(t, ok)
80+
81+
cfg.APIDetails.URL = server.URL + "/status"
82+
// Use the self-signed certificate for verification
83+
cfg.APIDetails.Ca = certFile
84+
85+
scraper := NewScraper(receivertest.NewNopSettings(component.Type{}), cfg)
86+
87+
startErr := scraper.Start(context.Background(), componenttest.NewNopHost())
88+
require.NoError(t, startErr)
89+
90+
_, err = scraper.Scrape(context.Background())
91+
assert.NoError(t, err, "Scraping with self-signed certificate should succeed")
92+
})
93+
}
94+
95+
func TestStubStatusScraperUnixSocket(t *testing.T) {
96+
// Create a test server with a Unix domain socket
97+
handler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
98+
if req.URL.Path == "/status" {
99+
rw.WriteHeader(http.StatusOK)
100+
_, _ = rw.Write([]byte(`Active connections: 291
101+
server accepts handled requests
102+
16630948 16630946 31070465
103+
Reading: 6 Writing: 179 Waiting: 106
104+
`))
105+
106+
return
107+
}
108+
rw.WriteHeader(http.StatusNotFound)
109+
})
110+
111+
// Create a socket file in a temporary directory with a shorter path
112+
socketPath := "/tmp/nginx-test.sock"
113+
114+
// Clean up any existing socket file
115+
os.Remove(socketPath)
116+
117+
// Create a listener for the Unix socket
118+
listener, err := net.Listen("unix", socketPath)
119+
require.NoError(t, err, "Failed to create Unix socket listener")
120+
121+
// Create a test server with our custom listener
122+
server := &httptest.Server{
123+
Listener: listener,
124+
Config: &http.Server{Handler: handler},
125+
}
126+
127+
// Start the server
128+
server.Start()
129+
130+
// Ensure cleanup of the socket file
131+
t.Cleanup(func() {
132+
server.Close()
133+
os.Remove(socketPath)
134+
})
135+
136+
// Test with Unix socket
137+
t.Run("Test 1: Unix socket", func(t *testing.T) {
138+
cfg, ok := config.CreateDefaultConfig().(*config.Config)
139+
require.True(t, ok)
140+
141+
cfg.APIDetails.URL = "http://unix/status"
142+
cfg.APIDetails.Listen = "unix:" + socketPath
143+
144+
scraper := NewScraper(receivertest.NewNopSettings(component.Type{}), cfg)
145+
146+
startErr := scraper.Start(context.Background(), componenttest.NewNopHost())
147+
require.NoError(t, startErr)
148+
149+
_, err = scraper.Scrape(context.Background())
150+
assert.NoError(t, err)
151+
})
152+
}

internal/collector/nginxplusreceiver/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type APIDetails struct {
2929
URL string `mapstructure:"url"`
3030
Listen string `mapstructure:"listen"`
3131
Location string `mapstructure:"location"`
32+
Ca string `mapstructure:"ca"`
3233
}
3334

3435
// Validate checks if the receiver configuration is valid
@@ -59,6 +60,7 @@ func createDefaultConfig() component.Config {
5960
URL: "http://localhost:80/api",
6061
Listen: "localhost:80",
6162
Location: "/api",
63+
Ca: "",
6264
},
6365
MetricsBuilderConfig: metadata.DefaultMetricsBuilderConfig(),
6466
}

internal/collector/nginxplusreceiver/scraper.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ package nginxplusreceiver
66

77
import (
88
"context"
9+
"crypto/tls"
10+
"crypto/x509"
911
"fmt"
1012
"net"
1113
"net/http"
14+
"os"
1215
"strconv"
1316
"strings"
1417
"sync"
@@ -82,6 +85,28 @@ func (nps *NginxPlusScraper) ID() component.ID {
8285
func (nps *NginxPlusScraper) Start(_ context.Context, _ component.Host) error {
8386
endpoint := strings.TrimPrefix(nps.cfg.APIDetails.URL, "unix:")
8487
httpClient := http.DefaultClient
88+
caCertLocation := nps.cfg.APIDetails.Ca
89+
if caCertLocation != "" {
90+
nps.logger.Debug("Reading CA certificate", zap.Any("file_path", caCertLocation))
91+
caCert, err := os.ReadFile(caCertLocation)
92+
if err != nil {
93+
nps.logger.Error("Error starting NGINX stub status scraper. "+
94+
"Failed to read CA certificate", zap.Error(err))
95+
96+
return err
97+
}
98+
caCertPool := x509.NewCertPool()
99+
caCertPool.AppendCertsFromPEM(caCert)
100+
101+
httpClient = &http.Client{
102+
Transport: &http.Transport{
103+
TLSClientConfig: &tls.Config{
104+
RootCAs: caCertPool,
105+
MinVersion: tls.VersionTLS13,
106+
},
107+
},
108+
}
109+
}
85110
httpClient.Timeout = nps.cfg.ClientConfig.Timeout
86111

87112
if strings.HasPrefix(nps.cfg.APIDetails.Listen, "unix:") {

internal/collector/otel_collector_plugin.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,7 @@ func (oc *Collector) checkForNewReceivers(ctx context.Context, nginxConfigContex
419419
URL: nginxConfigContext.PlusAPI.URL,
420420
Listen: nginxConfigContext.PlusAPI.Listen,
421421
Location: nginxConfigContext.PlusAPI.Location,
422+
Ca: nginxConfigContext.PlusAPI.Ca,
422423
},
423424
CollectionInterval: defaultCollectionInterval,
424425
},

0 commit comments

Comments
 (0)