feat(client): add mTLS config (#665)

* feat: add mtls config to client

* feat: add mtls config to client

* Rework client tls configuration

* Rebase (#3)

* chore(deps): bump codecov/codecov-action from 3.1.6 to 4.0.1 (#671)

Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3.1.6 to 4.0.1.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v3.1.6...v4.0.1)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* feat(client): enhance HTTP client configuration with proxy support (#668)

* feat: enhance HTTP client configuration with proxy support

- Add `ProxyURL` field to `Config` struct with YAML tag
- Implement proxy URL parsing and setting in `getHTTPClient` method
- Add test case for `getHTTPClient` method with custom proxy URL setting
- Include `net/url` package in both `config.go` and `config_test.go` files

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>

* docs: enhance README with Proxy and OAuth2 Docs

- Remove empty lines from README.md
- Add documentation for proxy configuration in client examples
- Include YAML examples for client using a proxy, custom DNS resolver, OAuth2, and identity-aware proxy configurations in README.md

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>

* docs: add proxy client

Signed-off-by: appleboy <appleboy.tw@gmail.com>

* Update client/config.go

* Update README.md

* Update client/config_test.go

---------

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Signed-off-by: appleboy <appleboy.tw@gmail.com>
Co-authored-by: TwiN <twin@linux.com>

* chore: Update Go to 1.21

* chore(deps): bump github.com/prometheus/client_golang from 1.17.0 to 1.18.0 (#658)

chore(deps): bump github.com/prometheus/client_golang

Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.17.0 to 1.18.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.17.0...v1.18.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump github.com/gofiber/fiber/v2 from 2.49.2 to 2.52.1 (#682)

Bumps [github.com/gofiber/fiber/v2](https://github.com/gofiber/fiber) from 2.49.2 to 2.52.1.
- [Release notes](https://github.com/gofiber/fiber/releases)
- [Commits](https://github.com/gofiber/fiber/compare/v2.49.2...v2.52.1)

---
updated-dependencies:
- dependency-name: github.com/gofiber/fiber/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* docs(alerting): Fix wrong gitlab terminology (alert key vs. PAT) (#694)

Fix wrong term (alert key vs. PAT)

* chore(deps): bump github.com/TwiN/deepmerge from 0.2.0 to 0.2.1 (#684)

* chore(deps): bump github.com/TwiN/deepmerge from 0.2.0 to 0.2.1

Bumps [github.com/TwiN/deepmerge](https://github.com/TwiN/deepmerge) from 0.2.0 to 0.2.1.
- [Release notes](https://github.com/TwiN/deepmerge/releases)
- [Commits](https://github.com/TwiN/deepmerge/compare/v0.2.0...v0.2.1)

---
updated-dependencies:
- dependency-name: github.com/TwiN/deepmerge
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* build: Add `go mod tidy` in Dockerfile

* ci: Update Go to 1.20

* Update go.mod

* Update test.yml

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: TwiN <twin@linux.com>

* chore(deps): bump golang.org/x/oauth2 from 0.13.0 to 0.18.0 (#701)

Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.13.0 to 0.18.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.13.0...v0.18.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* feat: add mtls config to client

feat: add mtls config to client

Rework client tls configuration

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Signed-off-by: appleboy <appleboy.tw@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-authored-by: TwiN <twin@linux.com>
Co-authored-by: Salim B <git@salim.space>

* Rebase (#4)

* chore(deps): bump codecov/codecov-action from 3.1.6 to 4.0.1 (#671)

Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3.1.6 to 4.0.1.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v3.1.6...v4.0.1)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* feat(client): enhance HTTP client configuration with proxy support (#668)

* feat: enhance HTTP client configuration with proxy support

- Add `ProxyURL` field to `Config` struct with YAML tag
- Implement proxy URL parsing and setting in `getHTTPClient` method
- Add test case for `getHTTPClient` method with custom proxy URL setting
- Include `net/url` package in both `config.go` and `config_test.go` files

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>

* docs: enhance README with Proxy and OAuth2 Docs

- Remove empty lines from README.md
- Add documentation for proxy configuration in client examples
- Include YAML examples for client using a proxy, custom DNS resolver, OAuth2, and identity-aware proxy configurations in README.md

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>

* docs: add proxy client

Signed-off-by: appleboy <appleboy.tw@gmail.com>

* Update client/config.go

* Update README.md

* Update client/config_test.go

---------

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Signed-off-by: appleboy <appleboy.tw@gmail.com>
Co-authored-by: TwiN <twin@linux.com>

* chore: Update Go to 1.21

* chore(deps): bump github.com/prometheus/client_golang from 1.17.0 to 1.18.0 (#658)

chore(deps): bump github.com/prometheus/client_golang

Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.17.0 to 1.18.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.17.0...v1.18.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump github.com/gofiber/fiber/v2 from 2.49.2 to 2.52.1 (#682)

Bumps [github.com/gofiber/fiber/v2](https://github.com/gofiber/fiber) from 2.49.2 to 2.52.1.
- [Release notes](https://github.com/gofiber/fiber/releases)
- [Commits](https://github.com/gofiber/fiber/compare/v2.49.2...v2.52.1)

---
updated-dependencies:
- dependency-name: github.com/gofiber/fiber/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* docs(alerting): Fix wrong gitlab terminology (alert key vs. PAT) (#694)

Fix wrong term (alert key vs. PAT)

* chore(deps): bump github.com/TwiN/deepmerge from 0.2.0 to 0.2.1 (#684)

* chore(deps): bump github.com/TwiN/deepmerge from 0.2.0 to 0.2.1

Bumps [github.com/TwiN/deepmerge](https://github.com/TwiN/deepmerge) from 0.2.0 to 0.2.1.
- [Release notes](https://github.com/TwiN/deepmerge/releases)
- [Commits](https://github.com/TwiN/deepmerge/compare/v0.2.0...v0.2.1)

---
updated-dependencies:
- dependency-name: github.com/TwiN/deepmerge
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* build: Add `go mod tidy` in Dockerfile

* ci: Update Go to 1.20

* Update go.mod

* Update test.yml

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: TwiN <twin@linux.com>

* chore(deps): bump golang.org/x/oauth2 from 0.13.0 to 0.18.0 (#701)

Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.13.0 to 0.18.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.13.0...v0.18.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* feat: add mtls config to client

* feat: add mtls config to client

* Rework client tls configuration

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Signed-off-by: appleboy <appleboy.tw@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-authored-by: TwiN <twin@linux.com>
Co-authored-by: Salim B <git@salim.space>

* Rebase (#6)

* feat(tls): add mtls config to client (#189)

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Signed-off-by: appleboy <appleboy.tw@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-authored-by: TwiN <twin@linux.com>
Co-authored-by: Salim B <git@salim.space>
This commit is contained in:
Chris Smith 2024-04-09 18:41:37 -04:00 committed by GitHub
parent f54c45e20e
commit f93cebe715
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 205 additions and 4 deletions

View File

@ -417,8 +417,12 @@ the client used to send the request.
| `client.proxy-url` | The URL of the proxy to use for the client | `""` |
| `client.identity-aware-proxy` | Google Identity-Aware-Proxy client configuration. | `{}` |
| `client.identity-aware-proxy.audience` | The Identity-Aware-Proxy audience. (client-id of the IAP oauth2 credential) | required `""` |
| `client.tls.certificate-file` | Path to a client certificate (in PEM format) for mTLS configurations. | `""` |
| `client.tls.private-key-file` | Path to a client private key (in PEM format) for mTLS configurations. | `""` |
| `client.tls.renegotiation` | Type of renegotiation support to provide. (`never`, `freely`, `once`). | `"never"` |
| `client.network` | The network to use for ICMP endpoint client (`ip`, `ip4` or `ip6`). | `"ip"` |
> 📝 Some of these parameters are ignored based on the type of endpoint. For instance, there's no certificate involved
> in ICMP requests (ping), therefore, setting `client.insecure` to `true` for an endpoint of that type will not do anything.
@ -490,6 +494,22 @@ endpoints:
> 📝 Note that Gatus will use the [gcloud default credentials](https://cloud.google.com/docs/authentication/application-default-credentials) within its environment to generate the token.
This example shows you how you cna use the `client.tls` configuration to perform an mTLS query to a backend API:
```yaml
endpoints:
- name: website
url: "https://your.mtls.protected.app/health"
client:
tls:
certificate-file: /path/to/user_cert.pem
private-key-file: /path/to/user_key.pem
renegotiation: once
conditions:
- "[STATUS] == 200"
```
> 📝 Note that if running in a container, you must volume mount the certificate and key into the container.
### Alerting
Gatus supports multiple alerting providers, such as Slack and PagerDuty, and supports different alerts for each
@ -2059,6 +2079,19 @@ endpoints:
- "[STATUS] == 200"
```
### Proxy client configuration
You can configure a proxy for the client to use by setting the `proxy-url` parameter in the client configuration.
```yaml
endpoints:
- name: website
url: "https://twin.sh/health"
client:
proxy-url: http://proxy.example.com:8080
conditions:
- "[STATUS] == 200"
```
### How to fix 431 Request Header Fields Too Large error
Depending on where your environment is deployed and what kind of middleware or reverse proxy sits in front of Gatus,

View File

@ -2,6 +2,7 @@ package client
import (
"bytes"
"crypto/tls"
"io"
"net/http"
"testing"
@ -290,3 +291,46 @@ func TestQueryWebSocket(t *testing.T) {
t.Error("expected an error due to the target not being websocket-friendly")
}
}
func TestTlsRenegotiation(t *testing.T) {
tests := []struct {
name string
cfg TLSConfig
expectedConfig tls.RenegotiationSupport
}{
{
name: "default",
cfg: TLSConfig{CertificateFile: "../testdata/cert.pem", PrivateKeyFile: "../testdata/cert.key"},
expectedConfig: tls.RenegotiateNever,
},
{
name: "never",
cfg: TLSConfig{RenegotiationSupport: "never", CertificateFile: "../testdata/cert.pem", PrivateKeyFile: "../testdata/cert.key"},
expectedConfig: tls.RenegotiateNever,
},
{
name: "once",
cfg: TLSConfig{RenegotiationSupport: "once", CertificateFile: "../testdata/cert.pem", PrivateKeyFile: "../testdata/cert.key"},
expectedConfig: tls.RenegotiateOnceAsClient,
},
{
name: "freely",
cfg: TLSConfig{RenegotiationSupport: "freely", CertificateFile: "../testdata/cert.pem", PrivateKeyFile: "../testdata/cert.key"},
expectedConfig: tls.RenegotiateFreelyAsClient,
},
{
name: "not-valid-and-broken",
cfg: TLSConfig{RenegotiationSupport: "invalid", CertificateFile: "../testdata/cert.pem", PrivateKeyFile: "../testdata/cert.key"},
expectedConfig: tls.RenegotiateNever,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
tls := &tls.Config{}
tlsConfig := configureTLS(tls, test.cfg)
if tlsConfig.Renegotiation != test.expectedConfig {
t.Errorf("expected tls renegotiation to be %v, but got %v", test.expectedConfig, tls.Renegotiation)
}
})
}
}

View File

@ -26,6 +26,7 @@ var (
ErrInvalidDNSResolverPort = errors.New("invalid DNS resolver port")
ErrInvalidClientOAuth2Config = errors.New("invalid oauth2 configuration: must define all fields for client credentials flow (token-url, client-id, client-secret, scopes)")
ErrInvalidClientIAPConfig = errors.New("invalid Identity-Aware-Proxy configuration: must define all fields for Google Identity-Aware-Proxy programmatic authentication (audience)")
ErrInvalidClientTLSConfig = errors.New("invalid TLS configuration: certificate-file and private-key-file must be specified")
defaultConfig = Config{
Insecure: false,
@ -72,6 +73,9 @@ type Config struct {
// Network (ip, ip4 or ip6) for the ICMP client
Network string `yaml:"network"`
// TLS configuration (optional)
TLS *TLSConfig `yaml:"tls,omitempty"`
}
// DNSResolverConfig is the parsed configuration from the DNSResolver config string.
@ -94,6 +98,17 @@ type IAPConfig struct {
Audience string `yaml:"audience"` // e.g. "toto.apps.googleusercontent.com"
}
// TLSConfig is the configuration for mTLS configurations
type TLSConfig struct {
// CertificateFile is the public certificate for TLS in PEM format.
CertificateFile string `yaml:"certificate-file,omitempty"`
// PrivateKeyFile is the private key file for TLS in PEM format.
PrivateKeyFile string `yaml:"private-key-file,omitempty"`
RenegotiationSupport string `yaml:"renegotiation,omitempty"`
}
// ValidateAndSetDefaults validates the client configuration and sets the default values if necessary
func (c *Config) ValidateAndSetDefaults() error {
if c.Timeout < time.Millisecond {
@ -111,6 +126,11 @@ func (c *Config) ValidateAndSetDefaults() error {
if c.HasIAPConfig() && !c.IAPConfig.isValid() {
return ErrInvalidClientIAPConfig
}
if c.HasTlsConfig() {
if err := c.TLS.isValid(); err != nil {
return err
}
}
return nil
}
@ -156,6 +176,11 @@ func (c *Config) HasIAPConfig() bool {
return c.IAPConfig != nil
}
// HasTlsConfig returns true if the client has client certificate parameters
func (c *Config) HasTlsConfig() bool {
return c.TLS != nil && len(c.TLS.CertificateFile) > 0 && len(c.TLS.PrivateKeyFile) > 0
}
// isValid() returns true if the IAP configuration is valid
func (c *IAPConfig) isValid() bool {
return len(c.Audience) > 0
@ -166,8 +191,26 @@ func (c *OAuth2Config) isValid() bool {
return len(c.TokenURL) > 0 && len(c.ClientID) > 0 && len(c.ClientSecret) > 0 && len(c.Scopes) > 0
}
// isValid() returns nil if the client tls certificates are valid, otherwise returns an error
func (t *TLSConfig) isValid() error {
if len(t.CertificateFile) > 0 && len(t.PrivateKeyFile) > 0 {
_, err := tls.LoadX509KeyPair(t.CertificateFile, t.PrivateKeyFile)
if err != nil {
return err
}
return nil
}
return ErrInvalidClientTLSConfig
}
// GetHTTPClient return an HTTP client matching the Config's parameters.
func (c *Config) getHTTPClient() *http.Client {
tlsConfig := &tls.Config{
InsecureSkipVerify: c.Insecure,
}
if c.HasTlsConfig() && c.TLS.isValid() == nil {
tlsConfig = configureTLS(tlsConfig, *c.TLS)
}
if c.httpClient == nil {
c.httpClient = &http.Client{
Timeout: c.Timeout,
@ -175,9 +218,7 @@ func (c *Config) getHTTPClient() *http.Client {
MaxIdleConns: 100,
MaxIdleConnsPerHost: 20,
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: c.Insecure,
},
TLSClientConfig: tlsConfig,
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if c.IgnoreRedirect {
@ -281,3 +322,23 @@ func configureOAuth2(httpClient *http.Client, c OAuth2Config) *http.Client {
client.Timeout = httpClient.Timeout
return client
}
// configureTLS returns a TLS Config that will enable mTLS
func configureTLS(tlsConfig *tls.Config, c TLSConfig) *tls.Config {
clientTLSCert, err := tls.LoadX509KeyPair(c.CertificateFile, c.PrivateKeyFile)
if err != nil {
return nil
}
tlsConfig.Certificates = []tls.Certificate{clientTLSCert}
tlsConfig.Renegotiation = tls.RenegotiateNever
renegotionSupport := map[string]tls.RenegotiationSupport{
"once": tls.RenegotiateOnceAsClient,
"freely": tls.RenegotiateFreelyAsClient,
"never": tls.RenegotiateNever,
}
if val, ok := renegotionSupport[c.RenegotiationSupport]; ok {
tlsConfig.Renegotiation = val
}
return tlsConfig
}

View File

@ -106,3 +106,66 @@ func TestConfig_getHTTPClient_withCustomProxyURL(t *testing.T) {
t.Errorf("expected Config.ProxyURL to set the HTTP client's proxy to %s", proxyURL)
}
}
func TestConfig_TlsIsValid(t *testing.T) {
tests := []struct {
name string
cfg *Config
expectedErr bool
}{
{
name: "good-tls-config",
cfg: &Config{TLS: &TLSConfig{CertificateFile: "../testdata/cert.pem", PrivateKeyFile: "../testdata/cert.key"}},
expectedErr: false,
},
{
name: "missing-certificate-file",
cfg: &Config{TLS: &TLSConfig{CertificateFile: "doesnotexist", PrivateKeyFile: "../testdata/cert.key"}},
expectedErr: true,
},
{
name: "bad-certificate-file",
cfg: &Config{TLS: &TLSConfig{CertificateFile: "../testdata/badcert.pem", PrivateKeyFile: "../testdata/cert.key"}},
expectedErr: true,
},
{
name: "no-certificate-file",
cfg: &Config{TLS: &TLSConfig{CertificateFile: "", PrivateKeyFile: "../testdata/cert.key"}},
expectedErr: true,
},
{
name: "missing-private-key-file",
cfg: &Config{TLS: &TLSConfig{CertificateFile: "../testdata/cert.pem", PrivateKeyFile: "doesnotexist"}},
expectedErr: true,
},
{
name: "no-private-key-file",
cfg: &Config{TLS: &TLSConfig{CertificateFile: "../testdata/cert.pem", PrivateKeyFile: ""}},
expectedErr: true,
},
{
name: "bad-private-key-file",
cfg: &Config{TLS: &TLSConfig{CertificateFile: "../testdata/cert.pem", PrivateKeyFile: "../testdata/badcert.key"}},
expectedErr: true,
},
{
name: "bad-certificate-and-private-key-file",
cfg: &Config{TLS: &TLSConfig{CertificateFile: "../testdata/badcert.pem", PrivateKeyFile: "../testdata/badcert.key"}},
expectedErr: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := test.cfg.TLS.isValid()
if (err != nil) != test.expectedErr {
t.Errorf("expected the existence of an error to be %v, got %v", test.expectedErr, err)
return
}
if !test.expectedErr {
if test.cfg.TLS.isValid() != nil {
t.Error("cfg.TLS.isValid() returned an error even though no error was expected")
}
}
})
}
}