Skip to content

Commit

Permalink
feat: mTLS with client certificate (#3302)
Browse files Browse the repository at this point in the history
* Support mTLS with client certificate when configured.

* Fix the omitempty typo.

* Add check for missing cert as well.

* Added documentation for artifactory and upload, as well as how to run mkdocs locally

* set pip version to just 3.

* Added example to the full config.

* Remove the Pipfile and update documentation to mention the task.

* update language in doc about multiarch images.

Co-authored-by: Sheridan C Rawlins <scr@ouryahoo.com>
  • Loading branch information
scr-oath and Sheridan C Rawlins committed Aug 12, 2022
1 parent 5759d7f commit 53ed816
Show file tree
Hide file tree
Showing 9 changed files with 260 additions and 34 deletions.
52 changes: 36 additions & 16 deletions internal/http/http.go
Expand Up @@ -112,6 +112,19 @@ func CheckConfig(ctx *context.Context, upload *config.Upload, kind string) error
return misconfigured(kind, upload, "no certificate could be added from the specified trusted_certificates configuration")
}

if upload.ClientX509Cert != "" && upload.ClientX509Key == "" {
return misconfigured(kind, upload, "'client_x509_key' must be set when 'client_x509_cert' is set")
}
if upload.ClientX509Key != "" && upload.ClientX509Cert == "" {
return misconfigured(kind, upload, "'client_x509_cert' must be set when 'client_x509_key' is set")
}
if upload.ClientX509Cert != "" && upload.ClientX509Key != "" {
if _, err := tls.LoadX509KeyPair(upload.ClientX509Cert, upload.ClientX509Key); err != nil {
return misconfigured(kind, upload,
"client x509 certificate could not be loaded from the specified 'client_x509_cert' and 'client_x509_key'")
}
}

return nil
}

Expand Down Expand Up @@ -306,27 +319,34 @@ func newUploadRequest(ctx *context.Context, method, target, username, secret str
}

func getHTTPClient(upload *config.Upload) (*h.Client, error) {
if upload.TrustedCerts == "" {
if upload.TrustedCerts == "" && upload.ClientX509Cert == "" && upload.ClientX509Key == "" {
return h.DefaultClient, nil
}
pool, err := x509.SystemCertPool()
if err != nil {
if runtime.GOOS == "windows" {
// on windows ignore errors until golang issues #16736 & #18609 get fixed
pool = x509.NewCertPool()
} else {
transport := &h.Transport{
Proxy: h.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{},
}
if upload.TrustedCerts != "" {
pool, err := x509.SystemCertPool()
if err != nil {
if runtime.GOOS == "windows" {
// on windows ignore errors until golang issues #16736 & #18609 get fixed
pool = x509.NewCertPool()
} else {
return nil, err
}
}
pool.AppendCertsFromPEM([]byte(upload.TrustedCerts)) // already validated certs checked by CheckConfig
transport.TLSClientConfig.RootCAs = pool
}
if upload.ClientX509Cert != "" && upload.ClientX509Key != "" {
cert, err := tls.LoadX509KeyPair(upload.ClientX509Cert, upload.ClientX509Key)
if err != nil {
return nil, err
}
transport.TLSClientConfig.Certificates = []tls.Certificate{cert}
}
pool.AppendCertsFromPEM([]byte(upload.TrustedCerts)) // already validated certs checked by CheckConfig
return &h.Client{
Transport: &h.Transport{
Proxy: h.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{ // nolint: gosec
RootCAs: pool,
},
},
}, nil
return &h.Client{Transport: transport}, nil
}

// executeHTTPRequest processes the http call with respect of context ctx.
Expand Down
74 changes: 59 additions & 15 deletions internal/http/http_test.go
Expand Up @@ -2,6 +2,7 @@ package http

import (
"bytes"
"crypto/tls"
"encoding/pem"
"errors"
"fmt"
Expand Down Expand Up @@ -566,6 +567,47 @@ func TestUpload(t *testing.T) {
check{"/blah/2.1.0/a.deb", "u3", "x", content, map[string]string{}},
),
},
{
name: "given a server with ClientAuth = RequireAnyClientCert, " +
"and an Upload with ClientX509Cert and ClientX509Key set, " +
"then the response should pass",
tryTLS: true,
setup: func(s *httptest.Server) (*context.Context, config.Upload) {
s.TLS.ClientAuth = tls.RequireAnyClientCert
return ctx, config.Upload{
Mode: ModeArchive,
Name: "a",
Target: s.URL + "/{{.ProjectName}}/{{.Version}}/",
Username: "u3",
TrustedCerts: cert(s),
ClientX509Cert: "testcert.pem",
ClientX509Key: "testkey.pem",
Exts: []string{"deb", "rpm"},
}
},
check: checks(
check{"/blah/2.1.0/a.deb", "u3", "x", content, map[string]string{}},
),
},
{
name: "given a server with ClientAuth = RequireAnyClientCert, " +
"and an Upload without either ClientX509Cert or ClientX509Key set, " +
"then the response should fail",
tryTLS: true,
setup: func(s *httptest.Server) (*context.Context, config.Upload) {
s.TLS.ClientAuth = tls.RequireAnyClientCert
return ctx, config.Upload{
Mode: ModeArchive,
Name: "a",
Target: s.URL + "/{{.ProjectName}}/{{.Version}}/",
Username: "u3",
TrustedCerts: cert(s),
Exts: []string{"deb", "rpm"},
}
},
wantErrTLS: true,
check: checks(),
},
}

uploadAndCheck := func(t *testing.T, setup func(*httptest.Server) (*context.Context, config.Upload), wantErrPlain, wantErrTLS bool, check func(r []*h.Request) error, srv *httptest.Server) {
Expand All @@ -585,21 +627,23 @@ func TestUpload(t *testing.T) {
}

for _, tt := range tests {
if tt.tryPlain {
t.Run(tt.name, func(t *testing.T) {
srv := httptest.NewServer(mux)
defer srv.Close()
uploadAndCheck(t, tt.setup, tt.wantErrPlain, tt.wantErrTLS, tt.check, srv)
})
}
if tt.tryTLS {
t.Run(tt.name+"-tls", func(t *testing.T) {
srv := httptest.NewUnstartedServer(mux)
srv.StartTLS()
defer srv.Close()
uploadAndCheck(t, tt.setup, tt.wantErrPlain, tt.wantErrTLS, tt.check, srv)
})
}
t.Run(tt.name, func(t *testing.T) {
if tt.tryPlain {
t.Run(tt.name, func(t *testing.T) {
srv := httptest.NewServer(mux)
defer srv.Close()
uploadAndCheck(t, tt.setup, tt.wantErrPlain, tt.wantErrTLS, tt.check, srv)
})
}
if tt.tryTLS {
t.Run(tt.name+"-tls", func(t *testing.T) {
srv := httptest.NewUnstartedServer(mux)
srv.StartTLS()
defer srv.Close()
uploadAndCheck(t, tt.setup, tt.wantErrPlain, tt.wantErrTLS, tt.check, srv)
})
}
})
}
}

Expand Down
32 changes: 32 additions & 0 deletions internal/http/testcert.pem
@@ -0,0 +1,32 @@
-----BEGIN CERTIFICATE-----
MIIFgTCCA2mgAwIBAgIUScqw7e1i0RlxSe+l4VSg6cJXjSAwDQYJKoZIhvcNAQEL
BQAwUDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMSEwHwYDVQQKDBhJbnRlcm5l
dCBXaWRnaXRzIFB0eSBMdGQxETAPBgNVBAMMCHRlc3RjZXJ0MB4XDTIyMDgxMDA3
NTMzN1oXDTIyMDkwOTA3NTMzN1owUDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNB
MSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxETAPBgNVBAMMCHRl
c3RjZXJ0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0I10EpRJapUo
hQrvTvK12utZ1L6UASW7twzN2l4alM/hRi9qyrUUWIhD8uB0oEaPxa8ErAJFuK2e
paNMB7j0bS4iMaaTZx7hh9IoEz8iROiBILt+68LM9zEVN19YbLws6m7IQ7LAcjj8
imshswzVIDyOArLSko8z81nEE8fAbzXzBRfG5+x4T5JnVTy/B4qNC3Rk5McNfsOj
bTUklsVOeOmsWoNsMZXUgPMKXZbwQ1fJlNEcXalfxWXF7MBVuZD59eKeHZdFCvg9
otVqdaD4tEDcm9rjd7osNEasdGPSGG6kNIbUE8LYmSTR3OL1oPoQiqi+ic9NUOer
lsUgjbHwH8B0arw7QIbNDLNgIsKJX5FuGfb6BgfWoItrGc2wqFvXAWVn0EmTGCd6
x7IioW/U4TI+WHKlSZ2PdwtGnEmp0JXzwx3n6XTja30DMZXXTZ4MXd+YdLn7Lajz
33BXm6UYiVpxkCD/3QN2+32SwYWYa/js7rf2gZ3G3lpt4Iqb82v4/p4wxRcUl5sj
ws8yteV15iecXP1ow/wmUfzLBmHQkwy9WD1poKGWL7fVzWpTe4U+lYM47mXznvzu
WkXI2K1/70L+IablW6USDdMolf4ZR8IOS8cXl30z1XP7c+u0V9SGHOLVToOEfGIx
zxgT2P8wfkAiDY/qhNT2R5nMVrSymm8CAwEAAaNTMFEwHQYDVR0OBBYEFABsCi9C
i5QxArCJBWh1R1Tw4ntLMB8GA1UdIwQYMBaAFABsCi9Ci5QxArCJBWh1R1Tw4ntL
MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBAEC/KYL0s/GYKtEY
PsDjF+JwJ2dAKf8ZzfykTUTH4MWA1aGVFlRs5K/pbr1r4nPNumhPyw0+q7YNE8oK
0rhL4LarZQNIPWfSybjxyKD4jQJE8qTFnu0imn9r8Lmqjwz6xoz5JPt89dzk8ysR
et7Yv4q0aFDffSTnx2JbabR53TXm6JqTFkEBn66GGyq7ZTVU0yaHUsWsWRBsLFLY
F/gQp+l8uJlEi9MQh0gWPeIUJf+uGryOsOTEpFvYQ/9kaMHaHDQQ4FdKMDpGP4xi
YNvCzn4xQkEX8r5+Kff0Nr9dflsg6TMzPUJwPKqfi6s+mj5AkB2rAiBKO/Yvu5sb
ZLfYiRP73TIj2PSi0OxhADhtBwkhYFzhnNlAF76QieXsRMnwDG0oztTxMKIcIXaW
cCwB637h+BypnR22ye8ObzCRvh7CW841Xb/qNaPsHiviEtPsejeMIXcSwVbk3cU+
zYPUYrg7+S6/BUQIcYX8sVuPHxmZlDe4Zt/wHn7PhvO7RkHKmEM+WH1snJ/0mxd7
V++YDMxBdThi33cNsfBT6ug9xEcLydSD+Q+VqOTS+YsrnvpdKl2l/A9pODcCpazn
xR/LjDsbbsHgCP+90tM46ZIS7HV5uT6Gyek/UEergtaNjmA6gFU1C8BPI2NZjn+n
BsplATncX8RY04Q6e6x4FMxLEGCK
-----END CERTIFICATE-----
52 changes: 52 additions & 0 deletions internal/http/testkey.pem
@@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
xCjEgdOkFCQ01FTbkkb4n7xPsleEas0=
-----END PRIVATE KEY-----
2 changes: 2 additions & 0 deletions pkg/config/config.go
Expand Up @@ -864,6 +864,8 @@ type Upload struct {
Mode string `yaml:"mode,omitempty" json:"mode,omitempty"`
Method string `yaml:"method,omitempty" json:"method,omitempty"`
ChecksumHeader string `yaml:"checksum_header,omitempty" json:"checksum_header,omitempty"`
ClientX509Cert string `yaml:"client_x509_cert" json:"client_x509_cert"`
ClientX509Key string `yaml:"client_x509_key" json:"client_x509_key"`
TrustedCerts string `yaml:"trusted_certificates,omitempty" json:"trusted_certificates,omitempty"`
Checksum bool `yaml:"checksum,omitempty" json:"checksum,omitempty"`
Signature bool `yaml:"signature,omitempty" json:"signature,omitempty"`
Expand Down
21 changes: 21 additions & 0 deletions www/README.md
@@ -0,0 +1,21 @@
# Documentation

Documentation is written in mkdocs and there are a few extensions that allow richer
authoring than markdown.

To iterate with documentation, therefore, it is recommended to run the mkdocs server and view your pages in a browser.

## Prerequisites

- [Get Docker](https://docs.docker.com/get-docker/)
- [Get Task](https://taskfile.dev/installation/)

### NOTE to M1/M2 mac owners

If running on an arm64-based mac (M1 or M2, aka "Applie Silicon"), you may find this method quite slow. Until
multiarch docker images can be built and made available, you may wish to build your own via:

```bash
git clone git@github.com:squidfunk/mkdocs-material.git
docker build -t docker.io/squidfunk/mkdocs-material .
```
22 changes: 21 additions & 1 deletion www/docs/customization/artifactory.md
Expand Up @@ -22,7 +22,7 @@ artifactories:
Prerequisites:

- A running Artifactory instances
- A user + password / API key with grants to upload an artifact
- A user + password / client x509 certificate / API key with grants to upload an artifact

### Target

Expand Down Expand Up @@ -82,6 +82,22 @@ If your instance is named `production`, you need to store the secret in the
environment variable `ARTIFACTORY_PRODUCTION_SECRET`.
The name will be transformed to uppercase.

### Client authorization with x509 certificate (mTLS / mutual TLS)

If your artifactory server supports authorization with mTLS (client certificates), you can provide them by specifying
the location of an x509 certificate/key pair of pem-encode files.

```yaml
artifactories:
- name: production
target: http://<Your-Instance>:8081/artifactory/example-repo-local/{{ .ProjectName }}/{{ .Version }}/
client_x509_cert: path/to/client.cert.pem
client_x509_key: path/to/client.key.pem
```

This will offer the client certificate during the TLS handshake, which your artifactory server may use to authenticate
and authorize you to upload.

### Server authentication

You can authenticate your Artifactory TLS server adding a trusted X.509
Expand Down Expand Up @@ -148,6 +164,10 @@ artifactories:
# User that will be used for the deployment
username: deployuser

# Client certificate and key (when provided, added as client cert to TLS connections)
client_x509_cert: /path/to/client.cert.pem
client_x509_key: /path/to/client.key.pem

# Upload checksums (defaults to false)
checksum: true

Expand Down
27 changes: 26 additions & 1 deletion www/docs/customization/upload.md
Expand Up @@ -20,7 +20,12 @@ uploads:
Prerequisites:

- An HTTP server accepting HTTP requests
- A user + password with grants to upload an artifact using HTTP requests for basic authentication (only if the server requires it)
- A user + password / client x509 certificate / API key with grants to upload an artifact

!!! note
authentication is optional and may be provided if the server requires it
- user/pass is for Basic Authentication
- client x509 certificate is for mutual TLS authentication (aka "mTLS")

### Target

Expand Down Expand Up @@ -89,6 +94,22 @@ The name will be transformed to uppercase.

This field is optional and is used only for basic http authentication.

### Client authorization with x509 certificate (mTLS / mutual TLS)

If your artifactory server supports authorization with mTLS (client certificates), you can provide them by specifying
the location of an x509 certificate/key pair of pem-encode files.

```yaml
uploads:
- name: production
client_x509_cert: path/to/client.cert.pem
client_x509_key: path/to/client.key.pem
target: 'http://some.server/some/path/example-repo-local/{{ .ProjectName }}/{{ .Version }}/{{ .Os }}/{{ .Arch }}{{ if .Arm }}{{ .Arm }}{{ end }}'
```

This will offer the client certificate during the TLS handshake, which your artifactory server may use to authenticate
and authorize you to upload.

### Server authentication

You can authenticate your TLS server adding a trusted X.509 certificate chain
Expand Down Expand Up @@ -166,6 +187,10 @@ uploads:
# An optional username that will be used for the deployment for basic authn
username: deployuser

# Client certificate and key (when provided, added as client cert to TLS connections)
client_x509_cert: /path/to/client.cert.pem
client_x509_key: /path/to/client.key.pem

# An optional header you can use to tell GoReleaser to pass the artifact's
# SHA256 checksum within the upload request.
# Default is empty.
Expand Down
12 changes: 11 additions & 1 deletion www/docs/static/schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 53ed816

Please sign in to comment.