Skip to content

Commit

Permalink
supporting mTLS for OPDK (#172)
Browse files Browse the repository at this point in the history
* supporting mTLS for OPDK

* changed flag comment
  • Loading branch information
rockspore authored Feb 22, 2021
1 parent 1191378 commit 632bcfc
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 7 deletions.
25 changes: 21 additions & 4 deletions apigee/edge_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package apigee
import (
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -131,7 +132,7 @@ type EdgeClientOptions struct {
// Specify the Edge organization name.
Org string

//Specify the Edge environment name.
// Specify the Edge environment name.
Env string

// Required. Authentication information for the Edge Management server.
Expand All @@ -145,6 +146,12 @@ type EdgeClientOptions struct {

// Optional. Skip cert verification.
InsecureSkipVerify bool

// Root CAs for mTLS connection
RootCAs *x509.CertPool

// TLS Certificates for mTLS connection
Certificates []tls.Certificate
}

// EdgeAuth holds information about how to authenticate to the Edge Management server.
Expand Down Expand Up @@ -201,12 +208,22 @@ func retrieveAuthFromNetrc(netrcPath, host string) (*EdgeAuth, error) {
func NewEdgeClient(o *EdgeClientOptions) (*EdgeClient, error) {
httpClient := http.DefaultClient

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

if o.InsecureSkipVerify {
tr := http.DefaultTransport.(*http.Transport).Clone()
tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
httpClient = &http.Client{Transport: tr}
tr.TLSClientConfig.InsecureSkipVerify = true
}

// config mTLS
if o.RootCAs != nil {
tr.TLSClientConfig.RootCAs = o.RootCAs
}
if len(o.Certificates) != 0 {
tr.TLSClientConfig.Certificates = o.Certificates
}

httpClient.Transport = tr

mgmtURL := o.MgmtURL
if o.MgmtURL == "" {
mgmtURL = defaultBaseURL
Expand Down
80 changes: 80 additions & 0 deletions apigee/edge_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
package apigee

import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"net/http"
"net/http/httptest"
Expand Down Expand Up @@ -221,3 +223,81 @@ func TestNetrcRetrieval(t *testing.T) {
auth.Username, auth.Password)
}
}

func TestMutualTLSWithCerts(t *testing.T) {
ts := newMutualTLSServer()
defer ts.Close()

caCertPool := x509.NewCertPool()
caCertPool.AddCert(ts.Certificate())

opts := &EdgeClientOptions{
MgmtURL: ts.URL,
Org: "org",
Env: "env",
RootCAs: caCertPool,
Certificates: ts.TLS.Certificates,
Auth: &EdgeAuth{
SkipAuth: true,
},
}

c, err := NewEdgeClient(opts)
if err != nil {
t.Fatal(err)
}

req, err := c.NewRequest(http.MethodGet, "", nil)
if err != nil {
t.Fatal(err)
}

_, err = c.Do(req, nil)
if err != nil {
t.Errorf("want no error got %v", err)
}
}

func TestMutualTLSNoCerts(t *testing.T) {
ts := newMutualTLSServer()
defer ts.Close()

opts := &EdgeClientOptions{
MgmtURL: ts.URL,
Org: "org",
Env: "env",
Auth: &EdgeAuth{
SkipAuth: true,
},
InsecureSkipVerify: true,
}

c, err := NewEdgeClient(opts)
if err != nil {
t.Fatal(err)
}

req, err := c.NewRequest(http.MethodGet, "", nil)
if err != nil {
t.Fatal(err)
}

_, err = c.Do(req, nil)
testutil.ErrorContains(t, err, "remote error: tls: bad certificate")
}

func newMutualTLSServer() *httptest.Server {
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte("{}"))
}))
// require mTLS
ts.TLS = &tls.Config{
RootCAs: x509.NewCertPool(),
ClientAuth: tls.RequireAnyClientCert,
}
ts.StartTLS()
ts.TLS.RootCAs.AddCert(ts.Certificate())

return ts
}
43 changes: 40 additions & 3 deletions shared/shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
package shared

import (
"errors"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"net/url"
Expand Down Expand Up @@ -85,6 +86,9 @@ type RootArgs struct {
ConfigPath string
InsecureSkipVerify bool
Namespace string
TLSCAFile string
TLSCertFile string
TLSKeyFile string

ServerConfig *server.Config // config loaded from ConfigPath

Expand Down Expand Up @@ -116,6 +120,13 @@ func AddCommandWithFlags(c *cobra.Command, rootArgs *RootArgs, cmds ...*cobra.Co
subC.PersistentFlags().BoolVarP(&rootArgs.InsecureSkipVerify, "insecure", "",
false, "allow insecure server connections when using SSL")

subC.PersistentFlags().StringVarP(&rootArgs.TLSCAFile, "tls-ca", "",
"", "path to the root CA for mTLS connection (opdk only)")
subC.PersistentFlags().StringVarP(&rootArgs.TLSCertFile, "tls-cert", "",
"", "path to the certificate for mTLS connection (opdk only)")
subC.PersistentFlags().StringVarP(&rootArgs.TLSKeyFile, "tls-key", "",
"", "path to the private key for mTLS connection (opdk only)")

c.AddCommand(subC)
}
}
Expand All @@ -128,7 +139,7 @@ func (r *RootArgs) Resolve(skipAuth, requireRuntime bool) error {
}

if r.IsLegacySaaS && r.IsOPDK {
return errors.New("--legacy and --opdk options are exclusive")
return fmt.Errorf("--legacy and --opdk options are exclusive")
}
r.IsGCPManaged = !(r.IsLegacySaaS || r.IsOPDK)

Expand Down Expand Up @@ -158,7 +169,7 @@ func (r *RootArgs) Resolve(skipAuth, requireRuntime bool) error {
}

if requireRuntime && r.RuntimeBase == "" {
return errors.New("--runtime is required for hybrid or opdk (or --organization and --environment with --legacy)")
return fmt.Errorf("--runtime is required for hybrid or opdk (or --organization and --environment with --legacy)")
}

// calculate internal proxy URL from runtime URL for LegacySaaS or OPDK
Expand Down Expand Up @@ -194,6 +205,32 @@ func (r *RootArgs) Resolve(skipAuth, requireRuntime bool) error {
InsecureSkipVerify: r.InsecureSkipVerify,
}

// config mTLS; only needed for OPDK
if r.IsOPDK {

// add given CA to the RootCAs
if r.TLSCAFile != "" {
caCert, err := os.ReadFile(r.TLSCAFile)
if err != nil {
return err
}
caCertPool := x509.NewCertPool()
if ok := caCertPool.AppendCertsFromPEM(caCert); !ok {
return fmt.Errorf("error appending CA to cert pool")
}
r.ClientOpts.RootCAs = caCertPool
}

// use given certs to configure client-side TLS
if r.TLSCertFile != "" && r.TLSKeyFile != "" {
cert, err := tls.LoadX509KeyPair(r.TLSCertFile, r.TLSKeyFile)
if err != nil {
return err
}
r.ClientOpts.Certificates = []tls.Certificate{cert}
}
}

var err error
r.ApigeeClient, err = apigee.NewEdgeClient(r.ClientOpts)
if err != nil {
Expand Down
43 changes: 43 additions & 0 deletions shared/shared_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,49 @@ func TestResolveAuth(t *testing.T) {
}
}

func TestResolveWithTLS(t *testing.T) {
r := &RootArgs{
IsOPDK: true,
Org: "hi",
Env: "test",
RuntimeBase: "runtime",
TLSCAFile: "./testdata/cert.pem",
TLSCertFile: "./testdata/cert.pem",
TLSKeyFile: "./testdata/key.pem",
}

if err := r.Resolve(true, true); err != nil {
t.Errorf("want no error got %v", err)
}

var want string

invalidCert, err := os.CreateTemp("", "")
if err != nil {
t.Fatal(err)
}
defer os.Remove(invalidCert.Name())

r.TLSCertFile = invalidCert.Name()
want = "tls: failed to find any PEM data in certificate input"
if err := r.Resolve(true, true); err == nil || err.Error() != want {
t.Errorf("want %s... got %v", want, err)
}

r.TLSCAFile = invalidCert.Name()
want = "error appending CA to cert pool"
if err := r.Resolve(true, true); err == nil || err.Error() != want {
t.Errorf("want %s... got %v", want, err)
}

r.TLSCAFile = "invalid-ca-path"

want = "open invalid-ca-path: no such file or directory"
if err := r.Resolve(true, true); err == nil || err.Error() != want {
t.Errorf("want %s... got %v", want, err)
}
}

func TestWrite(t *testing.T) {
var w io.Writer
b := []byte("test")
Expand Down
4 changes: 4 additions & 0 deletions shared/testdata/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
The cert files in this directory were created with the following command:
```
go run "$(go env GOROOT)/src/crypto/tls/generate_cert.go" --rsa-bits 1024 --host 127.0.0.1,::1,localhost --ca --start-date "Jan 1 00:00:00 2019" --duration=1000000h
```
14 changes: 14 additions & 0 deletions shared/testdata/cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-----BEGIN CERTIFICATE-----
MIICEjCCAXugAwIBAgIRAJyI4XwEYhd/WmXtG40IlBowDQYJKoZIhvcNAQELBQAw
EjEQMA4GA1UEChMHQWNtZSBDbzAgFw0xOTAxMDEwMDAwMDBaGA8yMTMzMDEyOTE2
MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
gYkCgYEAyW4e/XGus4dZceHuD3Cw07kE1bXWRpQtIiqWIs+Ol700dIBpNxr85ZUY
1eB/P7yAl+WiIgOwSXoYrzJmomW+SpYl25POBgdWePn9dDLJY1yRkvSG8Rw0N6sQ
V1iq1vyCQ+7nGopNo8UxgjVFqxoxLeuBQuaTTcp6bbrnF/VWqgsCAwEAAaNmMGQw
DgYDVR0PAQH/BAQDAgKkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1UdEwEB/wQF
MAMBAf8wLAYDVR0RBCUwI4IJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAA
AAABMA0GCSqGSIb3DQEBCwUAA4GBABb8soLXLHR7lqff1uch/9nTeFZMxWo7i+e4
uGoRiCjXNSUdQsK8Fn0KIU6dAg2a0sOZ8Sgtl+VbPPyh47D/5M5DwFEniA8UMATE
/KCvTeu4JffVS3jPmZX0af0IaOcaJP0IRrTkTRFTWbLmoi153K6wcq9+IsZhy1s0
ZaMjB1bl
-----END CERTIFICATE-----
16 changes: 16 additions & 0 deletions shared/testdata/key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-----BEGIN PRIVATE KEY-----
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAMluHv1xrrOHWXHh
7g9wsNO5BNW11kaULSIqliLPjpe9NHSAaTca/OWVGNXgfz+8gJfloiIDsEl6GK8y
ZqJlvkqWJduTzgYHVnj5/XQyyWNckZL0hvEcNDerEFdYqtb8gkPu5xqKTaPFMYI1
RasaMS3rgULmk03Kem265xf1VqoLAgMBAAECgYBNTWypOTqhfV0PPnR9CnNiHYxE
c+9S0MTtasiJfXwssZjy6OD4G+xYMzr/wZM0I6R6Js9tHFtIJ4pXmhEXW9KF4H3K
ZmIMs2slxRLbERv9ApE6+ddyZ5gBmtzD6qWHQ3qChQSjLVtKz4RIUHyKR602C4P2
s00ryAjWMm09Z8gl+QJBANInB+gzSkGqzYZhc01FcdtQZOjCgBJJP0anlENvMcws
W1yRlxcInIL/sly6v7GGXuT1i9GcxTJUKQAZItVQuU0CQQD1X/OO0KNZX2ZwYF25
ms2Zf1WmXVc+9tDpPDPlwX5l/Sbb2hY0SlRksuXLCYhWzyDk6nRjty598ZUD5Fy2
MQS3AkAm8CJv7Kjyl+Iy5vWFOLvK5g98bSVrvfSic8Rt5jl02jcnZLZ5Bxhw0U3M
DrIcA4irpa99bC3BkIR0RzQEEEv1AkBoDw4KJd7wWu3lgGie+tBwZTjceb8zO5az
Is3bhOhmtioRmHZMLK2Hmvqq1VsVfXe0vN0pIJk93gLVCLZsqXMXAkEAxXm9lrGX
zOzdmz0v9WJlHsP0bBz+WpGbI7ArQ7MuNkh1uvuQv3Cevdw2kw24sHpC9i5WzlJl
VTbu3q4nicI0MQ==
-----END PRIVATE KEY-----

0 comments on commit 632bcfc

Please sign in to comment.