Skip to content

Commit

Permalink
Flow Windows Certificate Store support added.
Browse files Browse the repository at this point in the history
  • Loading branch information
mattdurham committed Sep 12, 2023
1 parent 33b7b61 commit 24dbf02
Show file tree
Hide file tree
Showing 9 changed files with 314 additions and 65 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ internal API changes are not present.
Main (unreleased)
-----------------


> **BREAKING CHANGES**: This release has breaking changes. Please read entries
> carefully and consult the [upgrade guide][] for specific instructions.
### Breaking changes

- Static mode Windows Certificate Filter no longer restricted to TLS 1.2 and specific cipher suites. (@mattdurham)

### Features

- New Grafana Agent Flow components:
Expand All @@ -24,6 +32,8 @@ Main (unreleased)

- Clustering: Allow advertise interfaces to be configurable. (@wildum)

- Add support for `windows_certificate_filter` under http tls config block. (@mattdurham)

### Other changes

- Use Go 1.21.0 for builds. (@rfratto)
Expand Down
60 changes: 56 additions & 4 deletions docs/sources/flow/reference/config-blocks/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,17 @@ inner blocks.

The following blocks are supported inside the definition of `http`:

Hierarchy | Block | Description | Required
--------- | ----- | ----------- | --------
tls | [tls][] | Define TLS settings for the HTTP server. | no
Hierarchy | Block | Description | Required
--------- |--------------------------------|---------------------------------------------------------------| --------
tls | [tls][] | Define TLS settings for the HTTP server. | no
tls > windows_certificate_filter | [windows_certificate_filter][] | Configure Windows certificate store for all certificates. | no
tls > windows_certificate_filter > server | [server][] | Configure server certificates for Windows certificate filter. | no
tls > windows_certificate_filter > client | [client][] | Configure client certificates for Windows certificate filter. | no

[tls]: #tls-block
[windows_certificate_filter]: #windows-certificate-filter-block
[server]: #server-block
[client]: #client-block

### tls block

Expand All @@ -62,7 +68,7 @@ Grafana Agent.
Name | Type | Description | Default | Required
---- | ---- | ----------- | ------- | --------
`cert_pem` | `string` | PEM data of the server TLS certificate. | `""` | conditionally
`cert_file` | `string` | Path to the server TLS certificate on disk. | `""` | conditionally
`cert_file` | `string` | Path to the server TLS certificate on disk. | `""` | conditionallyy
`key_pem` | `string` | PEM data of the server TLS key. | `""` | conditionally
`key_file` | `string` | Path to the server TLS key on disk. | `""` | conditionally
`client_ca_pem` | `string` | PEM data of the client CA to validate requests against. | `""` | no
Expand Down Expand Up @@ -146,3 +152,49 @@ The following versions are recognized:
* `TLS12` for TLS 1.2
* `TLS11` for TLS 1.1
* `TLS10` for TLS 1.0


### windows certificate filter block

The `windows_certificate_filter` block is used to configure retrieving and client certificated from the built-in Windows
certificate store. This feature is not available on any platforms other than Windows. When using the `windows_certificate_filter` block
the following TLS settings are overridden and will cause an error when used.

* `cert_pem`
* `cert_file`
* `key_pem`
* `key_file`
* `client_ca`
* `client_ca_file`

{{% admonition type="warning" %}}
Also note TLS min and max may not be compatible with the certificate stored in the Windows certificate store. The `windows_certificate_filter`
will serve the found certificate even if it is not compatible with the specified TLS version.
{{% /admonition %}}


### server block

The `server` block is used to find the certificate to check the signer. If multiple are found the `windows_certificate_filter`
will choose the newest one that is current.

Name | Type | Description | Default | Required
---- |----------------|------------------------------------------------------------------------------------------|---------| --------
`store` | `string` | Name of the system store to look for the Server Certificate ex LocalMachine, CurrentUser. | `""` | conditionally
`system_store` | `string` | Name of the store to look for the Server Certificate ex My, CA. | `""` | conditionally
`issuer_common_names` | `list(string)` | Issuer common names to check against. | | no
`template_id` | `string` | Server Template ID to match in ASN1 format ex "1.2.3". | `""` | no
`refresh_interval` | `string` | How often to check for a new server certificate. | `"5m"` | no



### client block

The `client` block is used to check the certificate presented to the server.

Name | Type | Description | Default | Required
---- |----------------|--------------------------------------------------------|-----| --------
`issuer_common_names` | `list(string)` | Issuer common names to check against. | | no
`subject_regex` | `string` | Regular expression to match Subject name. | `""` | no
`template_id` | `string` | Client Template ID to match in ASN1 format ex "1.2.3". | | no

8 changes: 3 additions & 5 deletions pkg/server/tls.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package server

import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
Expand Down Expand Up @@ -154,8 +153,7 @@ type tlsListener struct {

innerListener net.Listener

windowsCertHandler *winCertStoreHandler
cancelWindowsCert context.CancelFunc //nolint
windowsCertHandler *WinCertStoreHandler
}

// newTLSListener creates and configures a new tlsListener.
Expand All @@ -182,8 +180,8 @@ func (l *tlsListener) Accept() (net.Conn, error) {
// Close implements net.Listener and closes the tlsListener, preventing any new
// connections from being formed. Existing connections will be kept alive.
func (l *tlsListener) Close() error {
if l.cancelWindowsCert != nil {
l.cancelWindowsCert()
if l.windowsCertHandler != nil {
l.windowsCertHandler.Stop()
}
return l.innerListener.Close()
}
Expand Down
118 changes: 73 additions & 45 deletions pkg/server/tls_certstore_windows.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package server

import (
"context"
"crypto"
"crypto/tls"
"crypto/x509"
Expand All @@ -16,8 +15,8 @@ import (
"time"
)

// winCertStoreHandler handles the finding of certificates, validating them and injecting into the default TLS pipeline
type winCertStoreHandler struct {
// WinCertStoreHandler handles the finding of certificates, validating them and injecting into the default TLS pipeline
type WinCertStoreHandler struct {
cfg WindowsCertificateFilter
subjectRegEx *regexp.Regexp
log log.Logger
Expand All @@ -30,8 +29,31 @@ type winCertStoreHandler struct {
// the client does NOT need the signer
serverIdentity certstore.Identity
clientAuth tls.ClientAuthType
shutdown chan struct{}
}

cancelContext context.Context
// NewWinCertStoreHandler creates a new WindowsCertificateFilter. Run should be called afterward.
func NewWinCertStoreHandler(cfg WindowsCertificateFilter, clientAuth tls.ClientAuthType, l log.Logger) (*WinCertStoreHandler, error) {
var subjectRegEx *regexp.Regexp
var err error
if cfg.Client != nil && cfg.Client.SubjectRegEx != "" {
subjectRegEx, err = regexp.Compile(cfg.Client.SubjectRegEx)
if err != nil {
return nil, fmt.Errorf("error compiling subject common name regular expression: %w", err)
}
}
cn := &WinCertStoreHandler{
cfg: cfg,
subjectRegEx: subjectRegEx,
log: l,
shutdown: make(chan struct{}),
}
err = cn.refreshCerts()
if err != nil {
return nil, err
}
cn.clientAuth = clientAuth
return cn, nil
}

func (l *tlsListener) applyWindowsCertificateStore(c TLSConfig) error {
Expand All @@ -56,45 +78,28 @@ func (l *tlsListener) applyWindowsCertificateStore(c TLSConfig) error {
}
}

// If there is an existing windows certhandler notify it to stop refreshing
if l.cancelWindowsCert != nil {
l.cancelWindowsCert()
l.cancelWindowsCert = nil
// If there is an existing windows certhandler stop it.
if l.windowsCertHandler != nil {
l.windowsCertHandler.Stop()
}
cancelCtx := context.Background()
cancelCtx, cancelHandler := context.WithCancel(cancelCtx)
l.cancelWindowsCert = cancelHandler
cn := &winCertStoreHandler{
cfg: *c.WindowsCertificateFilter,
subjectRegEx: subjectRegEx,
cancelContext: cancelCtx,
log: l.log,

cn := &WinCertStoreHandler{
cfg: *c.WindowsCertificateFilter,
subjectRegEx: subjectRegEx,
log: l.log,
shutdown: make(chan struct{}),
}

err = cn.refreshCerts()
if err != nil {
return err
}

config := &tls.Config{
VerifyPeerCertificate: cn.VerifyPeer,
GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
cn.winMut.Lock()
defer cn.winMut.Unlock()
cert := &tls.Certificate{
Certificate: [][]byte{cn.serverCert.Raw},
PrivateKey: cn.serverSigner,
Leaf: cn.serverCert,
// These seem to be the safest to use, tested on Win10, Server 2016, 2019, 2022
SupportedSignatureAlgorithms: []tls.SignatureScheme{
tls.PKCS1WithSHA512,
tls.PKCS1WithSHA384,
tls.PKCS1WithSHA256,
},
}
return cert, nil
},
// Windows has broad support for 1.2, only 2022 has support for 1.3
MaxVersion: tls.VersionTLS12,
GetCertificate: cn.CertificateHandler,
MaxVersion: uint16(c.MaxVersion),
MinVersion: uint16(c.MinVersion),
}

ca, err := getClientAuthFromString(c.ClientAuth)
Expand All @@ -111,8 +116,31 @@ func (l *tlsListener) applyWindowsCertificateStore(c TLSConfig) error {
return nil
}

// Run runs the filter refresh. Stop should be called when done.
func (c *WinCertStoreHandler) Run() {
go c.startUpdateTimer()
}

// Stop shuts down the refresh loop and frees the win32 handles.
func (c *WinCertStoreHandler) Stop() {
c.shutdown <- struct{}{}
}

// CertificateHandler returns the certificate to handle peer check.
func (c *WinCertStoreHandler) CertificateHandler(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
c.winMut.Lock()
defer c.winMut.Unlock()

cert := &tls.Certificate{
Certificate: [][]byte{c.serverCert.Raw},
PrivateKey: c.serverSigner,
Leaf: c.serverCert,
}
return cert, nil
}

// VerifyPeer is called by the TLS pipeline, and specified in tls.config, this is where any custom verification happens
func (c *winCertStoreHandler) VerifyPeer(_ [][]byte, verifiedChains [][]*x509.Certificate) error {
func (c *WinCertStoreHandler) VerifyPeer(_ [][]byte, verifiedChains [][]*x509.Certificate) error {
opts := x509.VerifyOptions{}
clientCert := verifiedChains[0][0]

Expand Down Expand Up @@ -150,7 +178,6 @@ func (c *winCertStoreHandler) VerifyPeer(_ [][]byte, verifiedChains [][]*x509.Ce
// call the normal pipeline
_, err := clientCert.Verify(opts)
return err

}

// this is the ASN1 Object Identifier for TemplateID
Expand All @@ -162,7 +189,7 @@ type templateInformation struct {
MinorVersion int
}

func (c *winCertStoreHandler) startUpdateTimer() {
func (c *WinCertStoreHandler) startUpdateTimer() {
refreshInterval := 5 * time.Minute
c.winMut.Lock()
if c.cfg.Server.RefreshInterval != 0 {
Expand All @@ -171,7 +198,7 @@ func (c *winCertStoreHandler) startUpdateTimer() {
c.winMut.Unlock()
for {
select {
case <-c.cancelContext.Done():
case <-c.shutdown:
if c.serverIdentity != nil {
c.serverIdentity.Close()
}
Expand All @@ -189,9 +216,10 @@ func (c *winCertStoreHandler) startUpdateTimer() {
}

// refreshCerts is the main work item in certificate store, responsible for finding the right certificate
func (c *winCertStoreHandler) refreshCerts() (err error) {
func (c *WinCertStoreHandler) refreshCerts() (err error) {
c.winMut.Lock()
defer c.winMut.Unlock()

level.Debug(c.log).Log("msg", "refreshing Windows certificates")
// Close the server identity if already set
if c.serverIdentity != nil {
Expand Down Expand Up @@ -226,12 +254,12 @@ func (c *winCertStoreHandler) refreshCerts() (err error) {
return
}

func (c *winCertStoreHandler) findServerIdentity() (certstore.Identity, error) {
func (c *WinCertStoreHandler) findServerIdentity() (certstore.Identity, error) {
return c.findCertificate(c.cfg.Server.SystemStore, c.cfg.Server.Store, c.cfg.Server.IssuerCommonNames, c.cfg.Server.TemplateID, nil, c.getStore)
}

// getStore converts the string representation to the enum representation
func (c *winCertStoreHandler) getStore(systemStore string, storeName string) (certstore.Store, error) {
func (c *WinCertStoreHandler) getStore(systemStore string, storeName string) (certstore.Store, error) {
st, err := certstore.StringToStoreType(systemStore)
if err != nil {
return nil, err
Expand All @@ -246,7 +274,7 @@ func (c *winCertStoreHandler) getStore(systemStore string, storeName string) (ce
type getStoreFunc func(systemStore, storeName string) (certstore.Store, error)

// findCertificate applies the filters to get the server certificate
func (c *winCertStoreHandler) findCertificate(systemStore string, storeName string, commonNames []string, templateID string, subjectRegEx *regexp.Regexp, getStore getStoreFunc) (certstore.Identity, error) {
func (c *WinCertStoreHandler) findCertificate(systemStore string, storeName string, commonNames []string, templateID string, subjectRegEx *regexp.Regexp, getStore getStoreFunc) (certstore.Identity, error) {
var store certstore.Store
var validIdentity certstore.Identity
var identities []certstore.Identity
Expand Down Expand Up @@ -307,7 +335,7 @@ func (c *winCertStoreHandler) findCertificate(systemStore string, storeName stri
return validIdentity, nil
}

func (c *winCertStoreHandler) filterByIssuerCommonNames(input []certstore.Identity, commonNames []string) ([]certstore.Identity, error) {
func (c *WinCertStoreHandler) filterByIssuerCommonNames(input []certstore.Identity, commonNames []string) ([]certstore.Identity, error) {
if len(commonNames) == 0 {
return input, nil
}
Expand All @@ -327,7 +355,7 @@ func (c *winCertStoreHandler) filterByIssuerCommonNames(input []certstore.Identi
return returnIdentities, nil
}

func (c *winCertStoreHandler) filterByTemplateID(input []certstore.Identity, id string) ([]certstore.Identity, error) {
func (c *WinCertStoreHandler) filterByTemplateID(input []certstore.Identity, id string) ([]certstore.Identity, error) {
if id == "" {
return input, nil
}
Expand Down Expand Up @@ -360,7 +388,7 @@ func getTemplateID(cert *x509.Certificate) string {
return ""
}

func (c *winCertStoreHandler) filterBySubjectRegularExpression(input []certstore.Identity, regEx *regexp.Regexp) ([]certstore.Identity, error) {
func (c *WinCertStoreHandler) filterBySubjectRegularExpression(input []certstore.Identity, regEx *regexp.Regexp) ([]certstore.Identity, error) {
if regEx == nil {
return input, nil
}
Expand Down
Loading

0 comments on commit 24dbf02

Please sign in to comment.