-
Notifications
You must be signed in to change notification settings - Fork 148
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
decrypt client mTLS certificate key for Elastic Defend
It adds EndpointTLSComponentModifier which will check if the client certificate key is encrypted, if so, it'll decrypt the key and pass it decrypted to endpoint (Elastic Defend)
- Loading branch information
Showing
6 changed files
with
710 additions
and
192 deletions.
There are no files selected for viewing
32 changes: 32 additions & 0 deletions
32
...26735182-Elastic-Defend-accepts-passphrase-protected-client-certificate-key-for-mTLS.yaml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
# Kind can be one of: | ||
# - breaking-change: a change to previously-documented behavior | ||
# - deprecation: functionality that is being removed in a later release | ||
# - bug-fix: fixes a problem in a previous version | ||
# - enhancement: extends functionality but does not break or fix existing behavior | ||
# - feature: new functionality | ||
# - known-issue: problems that we are aware of in a given version | ||
# - security: impacts on the security of a product or a user’s deployment. | ||
# - upgrade: important information for someone upgrading from a prior version | ||
# - other: does not fit into any of the other categories | ||
kind: feature | ||
|
||
# Change summary; a 80ish characters long description of the change. | ||
summary: Elastic Defend accepts passphrase protected client certificate key for mTLS | ||
|
||
# Long description; in case the summary is not enough to describe the change | ||
# this field accommodate a description without length limits. | ||
# NOTE: This field will be rendered only for breaking-change and known-issue kinds at the moment. | ||
#description: | ||
|
||
# Affected component; a word indicating the component this changeset affects. | ||
#component: | ||
|
||
# PR URL; optional; the PR number that added the changeset. | ||
# If not present is automatically filled by the tooling finding the PR where this changelog fragment has been added. | ||
# NOTE: the tooling supports backports, so it's able to fill the original PR number instead of the backport PR number. | ||
# Please provide it if you are adding a fragment for a different PR. | ||
#pr: https://github.com/owner/repo/1234 | ||
|
||
# Issue URL; optional; the GitHub issue related to this changeset (either closes or is part of). | ||
# If not present is automatically filled by the tooling with the issue linked to the PR number. | ||
issue: https://github.com/elastic/elastic-agent/issues/5490 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
208 changes: 208 additions & 0 deletions
208
internal/pkg/agent/application/endpoint_component_modifier.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
// or more contributor license agreements. Licensed under the Elastic License 2.0; | ||
// you may not use this file except in compliance with the Elastic License 2.0. | ||
|
||
package application | ||
|
||
import ( | ||
"encoding/pem" | ||
"errors" | ||
"fmt" | ||
"os" | ||
|
||
"github.com/elastic/elastic-agent-client/v7/pkg/client" | ||
"github.com/elastic/elastic-agent-libs/transport/tlscommon" | ||
"github.com/elastic/elastic-agent/internal/pkg/agent/application/coordinator" | ||
"github.com/elastic/elastic-agent/pkg/component" | ||
"github.com/elastic/elastic-agent/pkg/core/logger" | ||
) | ||
|
||
// EndpointSignedComponentModifier copies "signed" properties to the top level "signed" for the endpoint input. | ||
// Enpoint team want to be able to validate the signature and parse the signed configuration (not trust the agent). | ||
// Endpoint uses uninstall_token_hash in order to verify uninstall command token | ||
// and signing_key in order validate the action signature. | ||
// Example: | ||
// | ||
// { | ||
// .... | ||
// "signed": { | ||
// "data": "eyJpZCI6ImFhZWM4OTYwLWJiYjAtMTFlZC1hYzBkLTVmNjI0YTQxZjM4OCIsImFnZW50Ijp7InByb3RlY3Rpb24iOnsiZW5hYmxlZCI6dHJ1ZSwidW5pbnN0YWxsX3Rva2VuX2hhc2giOiIiLCJzaWduaW5nX2tleSI6Ik1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRW1tckhDSTdtZ2tuZUJlYVJkc2VkQXZBU2l0UHRLbnpPdUlzeHZJRWdGTkFLVlg3MWpRTTVmalo1eUdsSDB0TmJuR2JrU2pVM0VEVUZsOWllQ1J0ME5nPT0ifX19", | ||
// "signature": "MEUCIQCWoScyJW0dejHFxXBTEcSCOZiBHRVMjuJRPwFCwOdA1QIgKrtKUBzkvVeljRtJyMXfD8zIvWjrMzqhSkgjNESPW5E=" | ||
// }, | ||
// "revision": 1, | ||
// "type": "endpoint" | ||
// } | ||
func EndpointSignedComponentModifier() coordinator.ComponentsModifier { | ||
return func(comps []component.Component, cfg map[string]interface{}) ([]component.Component, error) { | ||
const signedKey = "signed" | ||
|
||
compIdx, unitIdx, ok := findEndpointUnit(comps, client.UnitTypeInput) | ||
if !ok { | ||
return comps, nil | ||
} | ||
|
||
unit := comps[compIdx].Units[unitIdx] | ||
unitCfgMap := unit.Config.Source.AsMap() | ||
if signed, ok := cfg[signedKey]; ok { | ||
unitCfgMap[signedKey] = signed | ||
} | ||
|
||
unitCfg, err := component.ExpectedConfig(unitCfgMap) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
unit.Config = unitCfg | ||
comps[compIdx].Units[unitIdx] = unit | ||
|
||
return comps, nil | ||
} | ||
} | ||
|
||
// EndpointTLSComponentModifier decrypts the client TLS certificate key if it's | ||
// passphrase-protected. It replaces the content of 'fleet.ssl.key' | ||
// and 'certificate' with theirs decrypted version and removes | ||
// 'key_passphrase_path'. | ||
// It does so, ONLY for the client TLS configuration for mTLS used with | ||
// fleet-server. | ||
func EndpointTLSComponentModifier(log *logger.Logger) coordinator.ComponentsModifier { | ||
return func(comps []component.Component, cfg map[string]interface{}) ([]component.Component, error) { | ||
compIdx, unitIdx, ok := findEndpointUnit(comps, client.UnitTypeInput) | ||
if !ok { | ||
// endpoint not present, nothing to do | ||
return comps, nil | ||
} | ||
|
||
unit := comps[compIdx].Units[unitIdx] | ||
unitCfgMap := unit.Config.Source.AsMap() | ||
|
||
// ensure the following config exists: | ||
// fleet.ssl: | ||
// key_passphrase_path | ||
// certificate | ||
// key | ||
fleetNode, ok := unitCfgMap["fleet"] | ||
if !ok { | ||
// if 'fleet' isn't, present nothing to do | ||
return comps, nil | ||
} | ||
fleetMap, ok := fleetNode.(map[string]interface{}) | ||
if !ok { | ||
return nil, fmt.Errorf("EndpointTLSComponentModifier: 'fleet' node isn't a map, it is: %T", fleetNode) | ||
} | ||
|
||
sslNode, ok := fleetMap["ssl"] | ||
if !ok { | ||
// 'ssl' node not present isn't an issue | ||
return comps, nil | ||
} | ||
sslMap, ok := sslNode.(map[string]interface{}) | ||
if !ok { | ||
return nil, fmt.Errorf("EndpointTLSComponentModifier: 'ssl' node isn't a map, it is: %T", sslNode) | ||
} | ||
|
||
keyPassPathI, ok := sslMap["key_passphrase_path"] | ||
if !ok { | ||
// if no key_passphrase_path, nothing to decrypt | ||
return comps, nil | ||
} | ||
keyPassPathStr, ok := keyPassPathI.(string) | ||
if !ok { | ||
return nil, errors.New("EndpointTLSComponentModifier: 'key_passphrase_path' isn't a string") | ||
} | ||
|
||
keyI, ok := sslMap["key"] | ||
if !ok { | ||
// if there is a key_passphrase_path, the key must be present | ||
return nil, errors.New("EndpointTLSComponentModifier: 'key_passphrase_path' present, but 'key' isn't present") | ||
} | ||
keyStr, ok := keyI.(string) | ||
if !ok { | ||
return nil, fmt.Errorf("EndpointTLSComponentModifier: 'key' isn't a string, it's %T", keyI) | ||
} | ||
|
||
certI, ok := sslMap["certificate"] | ||
if !ok { | ||
// if there is a key_passphrase_path, the certificate must be present | ||
return nil, errors.New("EndpointTLSComponentModifier: 'key_passphrase_path' present, but 'certificate' isn't present") | ||
} | ||
certStr, ok := certI.(string) | ||
if !ok { | ||
return nil, errors.New("EndpointTLSComponentModifier: 'certificate' isn't a string") | ||
} | ||
|
||
// all SSL config exists and the certificate key is passphrase protected, | ||
// now decrypt the key | ||
|
||
pass, err := os.ReadFile(keyPassPathStr) | ||
if err != nil { | ||
return nil, fmt.Errorf("EndpointTLSComponentModifier: failed to read client certificate passphrase file: %w", err) | ||
} | ||
|
||
// we don't really support encrypted certificates, but it's how | ||
// tlscommon.LoadCertificate does. Thus, let's keep the same behaviour. | ||
// Also, tlscommon.LoadCertificate 'loses' the type of the private key. | ||
// It stores they private key as an interface and there is no way to | ||
// retrieve the type os the private key without a type assertion. | ||
// Therefore, instead of manually checking the type, which would mean | ||
// to check for all supported private key types and keep it up to date, | ||
// better to load the certificate and its key directly from the PEM file. | ||
cert, err := tlscommon.ReadPEMFile(log, | ||
certStr, string(pass)) | ||
if err != nil { | ||
return nil, fmt.Errorf("EndpointTLSComponentModifier: unable to load TLS certificate: %w", err) | ||
} | ||
key, err := tlscommon.ReadPEMFile(log, | ||
keyStr, | ||
string(pass)) | ||
if err != nil { | ||
return nil, fmt.Errorf("EndpointTLSComponentModifier: unable to load TLS certificate key: %w", err) | ||
} | ||
|
||
// tlscommon.ReadPEMFile only removes the 'DEK-Info' header, not the | ||
// 'Proc-Type', so remove it now. Create a pem.Block to avoid editing | ||
// the PEM data manually: | ||
keyBlock, _ := pem.Decode(key) | ||
delete(keyBlock.Headers, "Proc-Type") | ||
key = pem.EncodeToMemory(keyBlock) | ||
|
||
// remove 'key_passphrase_path' as the certificate key isn't encrypted | ||
// anymore. | ||
sslMap["key_passphrase_path"] = "" | ||
|
||
// update the certificate and its key with their decrypted version. | ||
sslMap["certificate"] = string(cert) | ||
sslMap["key"] = string(key) | ||
|
||
unitCfg, err := component.ExpectedConfig(unitCfgMap) | ||
if err != nil { | ||
return nil, fmt.Errorf("EndpointTLSComponentModifier: could not covert modified config to expected config: %w", err) | ||
} | ||
|
||
unit.Config = unitCfg | ||
comps[compIdx].Units[unitIdx] = unit | ||
|
||
return comps, nil | ||
} | ||
} | ||
|
||
// findEndpointUnit finds the endpoint component and its unit of type 'unitType'. | ||
// It returns the component and unit index and true if found, if not, it returns | ||
// 0, 0, false. | ||
func findEndpointUnit(comps []component.Component, unitType client.UnitType) (int, int, bool) { | ||
// find the endpoint component | ||
for compIdx, comp := range comps { | ||
if comp.InputSpec != nil && comp.InputSpec.InputType != endpoint { | ||
continue | ||
} | ||
|
||
for unitIdx, unit := range comp.Units { | ||
if unit.Type != unitType { | ||
continue | ||
} | ||
|
||
return compIdx, unitIdx, true | ||
} | ||
} | ||
return 0, 0, false | ||
} |
Oops, something went wrong.