Skip to content

Commit 31dedaa

Browse files
authored
Add logstash to serverless provider (#1646)
1 parent 8a66d05 commit 31dedaa

File tree

12 files changed

+439
-114
lines changed

12 files changed

+439
-114
lines changed

internal/certs/certs.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ func LoadCA(certFile, keyFile string) (*Issuer, error) {
100100
}
101101

102102
func newCA(parent *Issuer) (*Issuer, error) {
103-
cert, err := New(true, parent)
103+
cert, err := New(true, false, parent)
104104
if err != nil {
105105
return nil, err
106106
}
@@ -115,13 +115,19 @@ func (i *Issuer) IssueIntermediate() (*Issuer, error) {
115115
// Issue issues a certificate with the given options. This certificate
116116
// can be used to configure a TLS server.
117117
func (i *Issuer) Issue(opts ...Option) (*Certificate, error) {
118-
return New(false, i, opts...)
118+
return New(false, false, i, opts...)
119+
}
120+
121+
// IssueClient issues a certificate with the given options. This certificate
122+
// can be used to configure a TLS client.
123+
func (i *Issuer) IssueClient(opts ...Option) (*Certificate, error) {
124+
return New(false, true, i, opts...)
119125
}
120126

121127
// NewSelfSignedCert issues a self-signed certificate with the given options.
122128
// This certificate can be used to configure a TLS server.
123129
func NewSelfSignedCert(opts ...Option) (*Certificate, error) {
124-
return New(false, nil, opts...)
130+
return New(false, false, nil, opts...)
125131
}
126132

127133
// Option is a function that can modify a certificate template. To be used
@@ -140,7 +146,7 @@ func WithName(name string) Option {
140146

141147
// New is the main helper to create a certificate, it is recommended to
142148
// use the more specific ones for specific use cases.
143-
func New(isCA bool, issuer *Issuer, opts ...Option) (*Certificate, error) {
149+
func New(isCA, isClient bool, issuer *Issuer, opts ...Option) (*Certificate, error) {
144150
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
145151
if err != nil {
146152
return nil, fmt.Errorf("failed to generate key: %w", err)
@@ -172,6 +178,15 @@ func New(isCA bool, issuer *Issuer, opts ...Option) (*Certificate, error) {
172178
} else {
173179
template.Subject.CommonName = "intermediate elastic-package CA"
174180
}
181+
// If the requester is a client we set clientAuth instead
182+
} else if isClient {
183+
template.ExtKeyUsage = []x509.ExtKeyUsage{
184+
x509.ExtKeyUsageClientAuth,
185+
}
186+
187+
// Include local hostname and ips as alternates in service certificates.
188+
template.DNSNames = []string{"localhost"}
189+
template.IPAddresses = []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")}
175190
} else {
176191
template.ExtKeyUsage = []x509.ExtKeyUsage{
177192
// Required for Chrome in OSX to show the "Proceed anyway" link.
@@ -313,8 +328,8 @@ func checkExpectedCertUsage(cert *x509.Certificate) error {
313328
if !cert.IsCA {
314329
// Required for Chrome in OSX to show the "Proceed anyway" link.
315330
// https://stackoverflow.com/a/64309893/28855
316-
if !containsExtKeyUsage(cert.ExtKeyUsage, x509.ExtKeyUsageServerAuth) {
317-
return fmt.Errorf("missing server auth key usage in certificate")
331+
if !(containsExtKeyUsage(cert.ExtKeyUsage, x509.ExtKeyUsageServerAuth) || containsExtKeyUsage(cert.ExtKeyUsage, x509.ExtKeyUsageClientAuth)) {
332+
return fmt.Errorf("missing either of server/client auth key usage in certificate")
318333
}
319334
}
320335

internal/kibana/fleet.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,20 @@ import (
1212
"time"
1313
)
1414

15+
type FleetOutput struct {
16+
ID string `json:"id,omitempty"`
17+
Name string `json:"name,omitempty"`
18+
Hosts []string `json:"hosts,omitempty"`
19+
Type string `json:"type,omitempty"`
20+
SSL *AgentSSL `json:"ssl,omitempty"`
21+
}
22+
23+
type AgentSSL struct {
24+
CertificateAuthorities []string `json:"certificate_authorities,omitempty"`
25+
Certificate string `json:"certificate,omitempty"`
26+
Key string `json:"key,omitempty"`
27+
}
28+
1529
// DefaultFleetServerURL returns the default Fleet server configured in Kibana
1630
func (c *Client) DefaultFleetServerURL() (string, error) {
1731
path := fmt.Sprintf("%s/fleet_server_hosts", FleetAPI)
@@ -45,6 +59,45 @@ func (c *Client) DefaultFleetServerURL() (string, error) {
4559
return "", errors.New("could not find the fleet server URL for this project")
4660
}
4761

62+
// UpdateFleetOutput updates an existing output to fleet
63+
// For example, to update ssl certificates etc.,
64+
func (c *Client) UpdateFleetOutput(fo FleetOutput, outputId string) error {
65+
reqBody, err := json.Marshal(fo)
66+
if err != nil {
67+
return fmt.Errorf("could not convert fleetOutput (request) to JSON: %w", err)
68+
}
69+
70+
statusCode, respBody, err := c.put(fmt.Sprintf("%s/outputs/%s", FleetAPI, outputId), reqBody)
71+
if err != nil {
72+
return fmt.Errorf("could not update fleet output: %w", err)
73+
}
74+
75+
if statusCode != http.StatusOK {
76+
return fmt.Errorf("could not update fleet output; API status code = %d; response body = %s", statusCode, respBody)
77+
}
78+
79+
return nil
80+
}
81+
82+
// AddFleetOutput adds an additional output to fleet eg., logstash
83+
func (c *Client) AddFleetOutput(fo FleetOutput) error {
84+
reqBody, err := json.Marshal(fo)
85+
if err != nil {
86+
return fmt.Errorf("could not convert fleetOutput (request) to JSON: %w", err)
87+
}
88+
89+
statusCode, respBody, err := c.post(fmt.Sprintf("%s/outputs", FleetAPI), reqBody)
90+
if err != nil {
91+
return fmt.Errorf("could not create fleet output: %w", err)
92+
}
93+
94+
if statusCode != http.StatusOK {
95+
return fmt.Errorf("could not add fleet output; API status code = %d; response body = %s", statusCode, respBody)
96+
}
97+
98+
return nil
99+
}
100+
48101
func (c *Client) SetAgentLogLevel(agentID, level string) error {
49102
path := fmt.Sprintf("%s/agents/%s/actions", FleetAPI, agentID)
50103

internal/serverless/project.go

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,22 @@ import (
1111
"io"
1212
"net/http"
1313
"net/url"
14+
"os"
15+
"path/filepath"
1416
"strings"
1517
"time"
1618

1719
"github.com/elastic/elastic-package/internal/elasticsearch"
1820
"github.com/elastic/elastic-package/internal/kibana"
1921
"github.com/elastic/elastic-package/internal/logger"
22+
"github.com/elastic/elastic-package/internal/profile"
2023
"github.com/elastic/elastic-package/internal/registry"
2124
)
2225

26+
const (
27+
FleetLogstashOutput = "fleet-logstash-output"
28+
)
29+
2330
// Project represents a serverless project
2431
type Project struct {
2532
url string
@@ -131,6 +138,54 @@ func (p *Project) DefaultFleetServerURL(kibanaClient *kibana.Client) (string, er
131138
return fleetURL, nil
132139
}
133140

141+
func (p *Project) AddLogstashFleetOutput(profile *profile.Profile, kibanaClient *kibana.Client) error {
142+
logstashFleetOutput := kibana.FleetOutput{
143+
Name: "logstash-output",
144+
ID: FleetLogstashOutput,
145+
Type: "logstash",
146+
Hosts: []string{"logstash:5044"},
147+
}
148+
149+
if err := kibanaClient.AddFleetOutput(logstashFleetOutput); err != nil {
150+
return fmt.Errorf("failed to add logstash fleet output: %w", err)
151+
}
152+
153+
return nil
154+
}
155+
156+
func (p *Project) UpdateLogstashFleetOutput(profile *profile.Profile, kibanaClient *kibana.Client) error {
157+
certsDir := filepath.Join(profile.ProfilePath, "certs", "elastic-agent")
158+
159+
caFile, err := os.ReadFile(filepath.Join(certsDir, "ca-cert.pem"))
160+
if err != nil {
161+
return fmt.Errorf("failed to read ca certificate: %w", err)
162+
}
163+
164+
certFile, err := os.ReadFile(filepath.Join(certsDir, "cert.pem"))
165+
if err != nil {
166+
return fmt.Errorf("failed to read client certificate: %w", err)
167+
}
168+
169+
keyFile, err := os.ReadFile(filepath.Join(certsDir, "key.pem"))
170+
if err != nil {
171+
return fmt.Errorf("failed to read client certificate private key: %w", err)
172+
}
173+
174+
logstashFleetOutput := kibana.FleetOutput{
175+
SSL: &kibana.AgentSSL{
176+
CertificateAuthorities: []string{string(caFile)},
177+
Certificate: string(certFile),
178+
Key: string(keyFile),
179+
},
180+
}
181+
182+
if err := kibanaClient.UpdateFleetOutput(logstashFleetOutput, FleetLogstashOutput); err != nil {
183+
return fmt.Errorf("failed to update logstash fleet output: %w", err)
184+
}
185+
186+
return nil
187+
}
188+
134189
func (p *Project) getESHealth(ctx context.Context, elasticsearchClient *elasticsearch.Client) error {
135190
return elasticsearchClient.CheckHealth(ctx)
136191
}
@@ -177,7 +232,7 @@ func (p *Project) getFleetHealth(ctx context.Context) error {
177232
return nil
178233
}
179234

180-
func (p *Project) CreateAgentPolicy(stackVersion string, kibanaClient *kibana.Client) error {
235+
func (p *Project) CreateAgentPolicy(stackVersion string, kibanaClient *kibana.Client, outputId string) error {
181236
systemPackages, err := registry.Production.Revisions("system", registry.SearchOptions{
182237
KibanaVersion: strings.TrimSuffix(stackVersion, kibana.SNAPSHOT_SUFFIX),
183238
})
@@ -195,7 +250,9 @@ func (p *Project) CreateAgentPolicy(stackVersion string, kibanaClient *kibana.Cl
195250
Description: "Policy created by elastic-package",
196251
Namespace: "default",
197252
MonitoringEnabled: []string{"logs", "metrics"},
253+
DataOutputID: outputId,
198254
}
255+
199256
newPolicy, err := kibanaClient.CreatePolicy(policy)
200257
if err != nil {
201258
return fmt.Errorf("error while creating agent policy: %w", err)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
version: '2.3'
2+
services:
3+
elastic-agent:
4+
image: "{{ fact "agent_image" }}"
5+
healthcheck:
6+
test: "elastic-agent status"
7+
timeout: 2s
8+
start_period: 360s
9+
retries: 180
10+
interval: 5s
11+
hostname: docker-fleet-agent
12+
env_file: "./elastic-agent.env"
13+
volumes:
14+
- type: bind
15+
source: ../../../tmp/service_logs/
16+
target: /tmp/service_logs/
17+
# Mount service_logs under /run too as a testing workaround for the journald input (see elastic-package#1235).
18+
- type: bind
19+
source: ../../../tmp/service_logs/
20+
target: /run/service_logs/
21+
- "../certs/ca-cert.pem:/etc/ssl/certs/elastic-package.pem"
22+
23+
elastic-agent_is_ready:
24+
image: tianon/true
25+
depends_on:
26+
elastic-agent:
27+
condition: service_healthy
28+
29+
{{ $logstash_enabled := fact "logstash_enabled" }}
30+
{{ if eq $logstash_enabled "true" }}
31+
logstash:
32+
image: "{{ fact "logstash_image" }}"
33+
healthcheck:
34+
test: bin/logstash -t
35+
interval: 60s
36+
timeout: 50s
37+
retries: 5
38+
# logstash expects the key in pkcs8 format. Hence converting the key.pem to pkcs8 format using openssl.
39+
# Also logstash-filter-elastic_integration plugin is installed by default to run ingest pipelines in logstash.
40+
# elastic-package#1637 made improvements to enable logstash stats through port 9600.
41+
command: bash -c 'openssl pkcs8 -inform PEM -in /usr/share/logstash/config/certs/key.pem -topk8 -nocrypt -outform PEM -out /usr/share/logstash/config/certs/logstash.pkcs8.key && chmod 777 /usr/share/logstash/config/certs/logstash.pkcs8.key && if [[ ! $(bin/logstash-plugin list) == *"logstash-filter-elastic_integration"* ]]; then echo "Missing plugin logstash-filter-elastic_integration, installing now" && bin/logstash-plugin install logstash-filter-elastic_integration; fi && bin/logstash -f /usr/share/logstash/pipeline/logstash.conf'
42+
volumes:
43+
- "../certs/logstash:/usr/share/logstash/config/certs"
44+
- "./logstash.conf:/usr/share/logstash/pipeline/logstash.conf:ro"
45+
ports:
46+
- "127.0.0.1:5044:5044"
47+
- "127.0.0.1:9600:9600"
48+
environment:
49+
- xpack.monitoring.enabled=false
50+
- ELASTIC_USER={{ fact "username" }}
51+
- ELASTIC_PASSWORD={{ fact "password" }}
52+
- ELASTIC_HOSTS={{ fact "elasticsearch_host" }}
53+
54+
logstash_is_ready:
55+
image: tianon/true
56+
depends_on:
57+
logstash:
58+
condition: service_healthy
59+
{{ end }}

internal/stack/_static/serverless-elastic-agent.yml.tmpl

Lines changed: 0 additions & 26 deletions
This file was deleted.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
input {
2+
elastic_agent {
3+
port => 5044
4+
ssl_enabled => true
5+
ssl_certificate_authorities => ["/usr/share/logstash/config/certs/ca-cert.pem"]
6+
ssl_certificate => "/usr/share/logstash/config/certs/cert.pem"
7+
ssl_key => "/usr/share/logstash/config/certs/logstash.pkcs8.key"
8+
}
9+
}
10+
11+
12+
filter {
13+
elastic_integration {
14+
remove_field => ['@version']
15+
hosts => ["{{ fact "elasticsearch_host" }}"]
16+
username => '{{ fact "username" }}'
17+
password => '{{ fact "password" }}'
18+
ssl_enabled => true
19+
ssl_verification_mode => "full"
20+
}
21+
}
22+
23+
24+
output {
25+
elasticsearch {
26+
hosts => ["{{ fact "elasticsearch_host" }}"]
27+
user => '{{ fact "username" }}'
28+
password => '{{ fact "password" }}'
29+
ssl_enabled => true
30+
data_stream => "true"
31+
}
32+
}

0 commit comments

Comments
 (0)