Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CA file #323

Merged
merged 21 commits into from
Aug 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1866909
golangci: disable gochecknoglobals in tests
mmatczuk Jul 31, 2023
bce2ff6
fileurl: add test case for data scheme
mmatczuk Jul 31, 2023
9413bfb
readurl: change signature of ReadURL to return []byte and add dedicat…
mmatczuk Jul 31, 2023
5d946f2
readurl: add support for reading data scheme
mmatczuk Jul 31, 2023
4694023
readurl: add ReadFileOrBase64 as a base 64 enabled os.ReadFile wrapper
mmatczuk Jul 31, 2023
ae2079b
tls: add support for loading TLS certificate and key from base64 enco…
mmatczuk Jul 31, 2023
90cf3e2
tls: split TLSConfig to TLSClientConfig and TLSServerConfig
mmatczuk Jul 31, 2023
bff3ecc
tls: refactor LoadCertificateFromTLSConfig to ConfigureTLSConfig method
mmatczuk Jul 31, 2023
29a3f16
tls: add CAFiles to TLSClientConfig
mmatczuk Jul 31, 2023
98f9849
http_transport: add TLSHandshakeTimeout to TLSClientConfig
mmatczuk Jul 31, 2023
50c920e
bind: extract TLSClientConfig from HTTPTransportConfig
mmatczuk Jul 31, 2023
047a37b
bind: add ca-file flag
mmatczuk Jul 31, 2023
7ab4b07
bind: extract TLSServerConfig from HTTPServerConfig
mmatczuk Aug 1, 2023
18a2dfa
bind: remove tls prefix from tls-[cert,key]-file
mmatczuk Aug 1, 2023
ef1352c
e2e/certs: CA signed certificate generation
mmatczuk Aug 1, 2023
0da0f79
e2e: use generated certificates instead of insecure
mmatczuk Aug 1, 2023
64d70dc
gh: update go action to generate certificates
mmatczuk Aug 1, 2023
f78e844
bind: rename ca-file to cacert-file for better compatibility with curl
mmatczuk Aug 2, 2023
fad0324
gh: on CI failure dump docker-compose.yaml
mmatczuk Aug 2, 2023
9ab4444
http_proxy: log when using custom root CA certificates
mmatczuk Aug 2, 2023
2ec2171
http_proxy: log when using custom root CA certificates
mmatczuk Aug 2, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 10 additions & 3 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,19 @@ jobs:
with:
go-version: "${{env.GO_VERSION}}"

- name: Generate certificates
run: make -C e2e/certs certs

- name: Build Docker image
run: make update-devel-image

- name: Run e2e test
run: cd e2e && make run-e2e
run: make -C e2e run-e2e

- name: Docker Compose file
if: failure()
run: cat e2e/docker-compose.yaml

- name: Dump Logs
- name: Docker Logs
if: failure()
run: cd e2e && make dump-logs
run: make -C e2e dump-logs
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ issues:
linters:
- bodyclose
- funlen
- gochecknoglobals
- gocognit
- gomnd
- gosec
Expand Down
42 changes: 30 additions & 12 deletions bind/flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ func PAC(fs *pflag.FlagSet, pac **url.URL) {
fs.VarP(anyflag.NewValue[*url.URL](*pac, pac, fileurl.ParseFilePathOrURL),
"pac", "p", "<path or URL>"+
"Proxy Auto-Configuration file to use for upstream proxy selection. "+
"It can be a local file or a URL, you can also use '-' to read from stdin. ")
"It can be a local file or a URL, you can also use '-' to read from stdin. "+
"The data URI scheme is supported, the format is data:base64,<encoded data>. ")
}

func RequestHeaders(fs *pflag.FlagSet, headers *[]header.Header) {
Expand Down Expand Up @@ -109,9 +110,7 @@ func HTTPTransportConfig(fs *pflag.FlagSet, cfg *forwarder.HTTPTransportConfig)
"The maximum amount of time a dial will wait for a connect to complete. "+
"With or without a timeout, the operating system may impose its own earlier timeout. For instance, TCP timeouts are often around 3 minutes. ")

fs.DurationVar(&cfg.TLSHandshakeTimeout,
"http-tls-handshake-timeout", cfg.TLSHandshakeTimeout,
"The maximum amount of time waiting to wait for a TLS handshake. Zero means no limit.")
TLSClientConfig(fs, &cfg.TLSClientConfig)

fs.DurationVar(&cfg.IdleConnTimeout,
"http-idle-conn-timeout", cfg.IdleConnTimeout,
Expand All @@ -123,10 +122,23 @@ func HTTPTransportConfig(fs *pflag.FlagSet, cfg *forwarder.HTTPTransportConfig)
"The amount of time to wait for a server's response headers after fully writing the request (including its body, if any)."+
"This time does not include the time to read the response body. "+
"Zero means no limit. ")
}

fs.BoolVar(&cfg.TLSConfig.InsecureSkipVerify, "insecure", cfg.TLSConfig.InsecureSkipVerify,
func TLSClientConfig(fs *pflag.FlagSet, cfg *forwarder.TLSClientConfig) {
fs.DurationVar(&cfg.HandshakeTimeout,
"http-tls-handshake-timeout", cfg.HandshakeTimeout,
"The maximum amount of time waiting to wait for a TLS handshake. Zero means no limit.")

fs.BoolVar(&cfg.InsecureSkipVerify, "insecure", cfg.InsecureSkipVerify,
"Don't verify the server's certificate chain and host name. "+
"Enable to work with self-signed certificates. ")

fs.StringSliceVar(&cfg.CAFiles,
"cacert-file", cfg.CAFiles, "<path or base64>"+
"Add your own CA certificates to verify against. "+
"The system root certificates will be used in addition to any certificates in this list. "+
"Can be a path to a file or \"data:\" followed by base64 encoded certificate. "+
"Use this flag multiple times to specify multiple CA certificate files. ")
}

func HTTPServerConfig(fs *pflag.FlagSet, cfg *forwarder.HTTPServerConfig, prefix string, schemes ...forwarder.Scheme) {
Expand Down Expand Up @@ -167,13 +179,7 @@ func HTTPServerConfig(fs *pflag.FlagSet, cfg *forwarder.HTTPServerConfig, prefix
"For https and h2 protocols, if TLS certificate is not specified, "+
"the server will use a self-signed certificate. ")

fs.StringVar(&cfg.CertFile,
namePrefix+"tls-cert-file", cfg.CertFile, "<path>"+
"TLS certificate to use if the server protocol is https or h2. ")

fs.StringVar(&cfg.KeyFile,
namePrefix+"tls-key-file", cfg.KeyFile, "<path>"+
"TLS private key to use if the server protocol is https or h2. ")
TLSServerConfig(fs, &cfg.TLSServerConfig, namePrefix)
}

fs.DurationVar(&cfg.ReadHeaderTimeout,
Expand All @@ -200,6 +206,18 @@ func HTTPServerConfig(fs *pflag.FlagSet, cfg *forwarder.HTTPServerConfig, prefix
"Setting this to none disables logging. ")
}

func TLSServerConfig(fs *pflag.FlagSet, cfg *forwarder.TLSServerConfig, namePrefix string) {
fs.StringVar(&cfg.CertFile,
namePrefix+"cert-file", cfg.CertFile, "<path or base64>"+
"TLS certificate to use if the server protocol is https or h2. "+
"Can be a path to a file or \"data:\" followed by base64 encoded certificate. ")

fs.StringVar(&cfg.KeyFile,
namePrefix+"key-file", cfg.KeyFile, "<path or base64>"+
"TLS private key to use if the server protocol is https or h2. "+
"Can be a path to a file or \"data:\" followed by base64 encoded key. ")
}

func LogConfig(fs *pflag.FlagSet, cfg *log.Config) {
fs.VarP(anyflag.NewValue[*os.File](nil, &cfg.File,
forwarder.OpenFileParser(os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600, 0o700)),
Expand Down
7 changes: 5 additions & 2 deletions cmd/forwarder/pac/eval/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,12 @@ func (c *command) RunE(cmd *cobra.Command, args []string) error {
}
resolver = r
}
t := forwarder.NewHTTPTransport(c.httpTransportConfig, resolver)
t, err := forwarder.NewHTTPTransport(c.httpTransportConfig, resolver)
if err != nil {
return err
}

script, err := forwarder.ReadURL(c.pac, t)
script, err := forwarder.ReadURLString(c.pac, t)
if err != nil {
return fmt.Errorf("read PAC file: %w", err)
}
Expand Down
7 changes: 5 additions & 2 deletions cmd/forwarder/pac/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,12 @@ func (c *command) RunE(cmd *cobra.Command, args []string) error {
logger := stdlog.New(c.logConfig)
logger.Debugf("configuration\n%s", config)

t := forwarder.NewHTTPTransport(c.httpTransportConfig, nil)
t, err := forwarder.NewHTTPTransport(c.httpTransportConfig, nil)
if err != nil {
return err
}

script, err := forwarder.ReadURL(c.pac, t)
script, err := forwarder.ReadURLString(c.pac, t)
if err != nil {
return fmt.Errorf("read PAC file: %w", err)
}
Expand Down
8 changes: 6 additions & 2 deletions cmd/forwarder/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,12 @@ func rootCommand() *cobra.Command {
Prefix: []string{"dns"},
},
{
Name: "HTTP client options",
Prefix: []string{"http", "insecure"},
Name: "HTTP client options",
Prefix: []string{
"http",
"cacert-file",
"insecure",
},
},
{
Name: "Logging options",
Expand Down
8 changes: 6 additions & 2 deletions cmd/forwarder/run/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,16 @@ func (c *command) RunE(cmd *cobra.Command, args []string) error {
}
resolver = r
}
rt = forwarder.NewHTTPTransport(c.httpTransportConfig, resolver)
var err error
rt, err = forwarder.NewHTTPTransport(c.httpTransportConfig, resolver)
if err != nil {
return err
}
}

if c.pac != nil {
var err error
script, err = forwarder.ReadURL(c.pac, rt)
script, err = forwarder.ReadURLString(c.pac, rt)
if err != nil {
return fmt.Errorf("read PAC file: %w", err)
}
Expand Down
5 changes: 3 additions & 2 deletions e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

## Running the e2e tests with the test runner

1. Build the forwarder image `make -C ../ update-devel-image`
1. Start the test runner `make run-e2e`
1. Generate certificates `make -C certs certs`, this can be done once
1. Build the forwarder image `make -C ../ update-devel-image`, this needs to be done after each forwarder code change
2. Start the test runner `make run-e2e`
1. The test runner will run all the tests sequentially and output the results to the console
1. If one of the test fails, the procedure will stop, test output will be printed
1. Environment will not be pruned once the error occurred, remember to manually clean it up with `make down`
Expand Down
3 changes: 3 additions & 0 deletions e2e/certs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ca.srl
*.crt
*.key
7 changes: 7 additions & 0 deletions e2e/certs/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.PHONY: certs
certs:
@./gen.sh

.PHONY: test
test:
@go test -v -tags manual .
52 changes: 52 additions & 0 deletions e2e/certs/cert_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//go:build manual

package certs_test

import (
"crypto/tls"
"net/http"
"testing"

"github.com/saucelabs/forwarder"
)

func TestCertificate(t *testing.T) {
server := http.Server{
Addr: "127.0.0.1:8443",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, world!"))
}),
}
defer server.Close()

go server.ListenAndServeTLS("httpbin.crt", "httpbin.key")

tlsCfg := &tls.Config{
ServerName: "httpbin",
}
cfg := forwarder.TLSClientConfig{
CAFiles: []string{
"./ca.crt",
},
}
cfg.ConfigureTLSConfig(tlsCfg)

tr := http.DefaultTransport.(*http.Transport).Clone()
tr.TLSClientConfig = tlsCfg

req, err := http.NewRequest("GET", "https://"+server.Addr, http.NoBody)
if err != nil {
t.Fatal(err)
}

res, err := tr.RoundTrip(req)
if err != nil {
t.Fatal(err)
}

if res.StatusCode != http.StatusOK {
t.Fatal("unexpected status code:", res.StatusCode)
}

tr.CloseIdleConnections()
}
60 changes: 60 additions & 0 deletions e2e/certs/gen.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env bash

set -eu -o pipefail

# Common variables
CA_ORGANIZATION="Sauce Labs Inc."
CA_KEY="ca.key"
CA_CERT="ca.crt"
CA_SUBJECT="/C=US/O=${CA_ORGANIZATION}"

EC_CURVE="prime256v1"

# Generate CA key and self-signed certificate with SHA-256
openssl ecparam -genkey -name ${EC_CURVE} -out ${CA_KEY}
openssl req -new -x509 -sha256 -days 365 -nodes -key ${CA_KEY} -subj "${CA_SUBJECT}" -out ${CA_CERT} \
-extensions v3_ca -config <(cat /etc/ssl/openssl.cnf - << EOF
[v3_ca]
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer
basicConstraints=critical,CA:true
keyUsage=critical,keyCertSign,cRLSign
EOF
)

# Function to generate certificates for each host name
generate_certificate() {
local HOST_NAME="$1"
local KEY="${HOST_NAME}.key"
local CSR="${HOST_NAME}.csr"
local CERT="${HOST_NAME}.crt"
local SUBJECT="/C=US/O=${CA_ORGANIZATION}/CN=${HOST_NAME}"

# Generate host key and certificate signing request (CSR)
openssl ecparam -genkey -name ${EC_CURVE} -out ${KEY}
openssl req -new -key ${KEY} -subj "${SUBJECT}" -out ${CSR}

# Sign the CSR with the CA to generate the host certificate
openssl x509 -req -sha256 -days 365 -in ${CSR} -CA ${CA_CERT} -CAkey ${CA_KEY} -CAcreateserial -out ${CERT}\
-extensions v3_req -extfile <(cat /etc/ssl/openssl.cnf - << EOF
[v3_req]
basicConstraints=critical,CA:FALSE
authorityKeyIdentifier=keyid,issuer
subjectAltName=@alt_names
keyUsage=digitalSignature,keyEncipherment
[ alt_names ]
DNS.1 = ${HOST_NAME}
DNS.2 = localhost
EOF
)

# Remove the CSR (not needed anymore)
rm ${CSR}
}

# Generate certificates for each host name
generate_certificate "proxy"
generate_certificate "upstream-proxy"
generate_certificate "httpbin"

chmod 644 *.key *.crt
13 changes: 12 additions & 1 deletion e2e/forwarder/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,24 @@ func HttpbinService() *Service {

func (s *Service) WithProtocol(protocol string) *Service {
s.Environment["FORWARDER_PROTOCOL"] = protocol

if protocol == "https" || protocol == "h2" {
s.Environment["FORWARDER_CERT_FILE"] = "/etc/forwarder/certs/" + s.Name + ".crt"
s.Environment["FORWARDER_KEY_FILE"] = "/etc/forwarder/private/" + s.Name + ".key"
s.Volumes = append(s.Volumes,
"./certs/"+s.Name+".crt:/etc/forwarder/certs/"+s.Name+".crt:ro",
"./certs/"+s.Name+".key:/etc/forwarder/private/"+s.Name+".key:ro",
)
}

return s
}

func (s *Service) WithUpstream(name, protocol string) *Service {
s.Environment["FORWARDER_PROXY"] = protocol + "://" + name + ":3128"
if protocol == "https" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's preserve the same condition as in WithProtocol.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if protocol != "http" {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's https and h2 but sure I can change it.

s.Environment["FORWARDER_INSECURE"] = "true"
s.Environment["FORWARDER_CACERT_FILE"] = "/etc/forwarder/certs/ca-certificates.crt"
s.Volumes = append(s.Volumes, "./certs/ca.crt:/etc/forwarder/certs/ca-certificates.crt:ro")
}
return s
}
Expand Down
7 changes: 7 additions & 0 deletions fileurl/fileurl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ func TestParseFilePathOrURL(t *testing.T) {
Path: "/path/to/file",
},
},
{
input: "data:text/plain;base64,U2F1Y2VMYWJzCg==",
want: url.URL{
Scheme: "data",
Opaque: "text/plain;base64,U2F1Y2VMYWJzCg==",
},
},
}

for i := range tests {
Expand Down
10 changes: 5 additions & 5 deletions http_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,10 @@ func NewHTTPProxy(cfg *HTTPProxyConfig, pr PACResolver, cm *CredentialsMatcher,
if rt == nil {
log.Infof("HTTP transport not configured, using standard library default")
rt = http.DefaultTransport.(*http.Transport).Clone() //nolint:forcetypeassert // we know it's a *http.Transport
} else if _, ok := rt.(*http.Transport); !ok {
} else if tr, ok := rt.(*http.Transport); !ok {
log.Debugf("using custom HTTP transport %T", rt)
} else if tr.TLSClientConfig != nil && tr.TLSClientConfig.RootCAs != nil {
log.Infof("using custom root CA certificates")
}
hp := &HTTPProxy{
config: *cfg,
Expand All @@ -160,11 +162,9 @@ func (hp *HTTPProxy) configureHTTPS() error {
hp.log.Debugf("loading TLS certificate from %s and %s", hp.config.CertFile, hp.config.KeyFile)
}

tlsCfg := httpsTLSConfigTemplate()
err := LoadCertificateFromTLSConfig(tlsCfg, &hp.config.TLSConfig)
hp.TLSConfig = tlsCfg
hp.TLSConfig = httpsTLSConfigTemplate()

return err
return hp.config.ConfigureTLSConfig(hp.TLSConfig)
}

func (hp *HTTPProxy) configureProxy() {
Expand Down