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

Add root CA support for mTLS authentication #270

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ provider "restapi" {
- `insecure` (Boolean) When using https, this disables TLS verification of the host.
- `key_file` (String) When set with the cert_file parameter, the provider will load a client certificate as a file for mTLS authentication. Note that this mechanism simply delegates to golang's tls.LoadX509KeyPair which does not support passphrase protected private keys. The most robust security protections available to the key_file are simple file system permissions.
- `key_string` (String) When set with the cert_string parameter, the provider will load a client certificate as a string for mTLS authentication. Note that this mechanism simply delegates to golang's tls.LoadX509KeyPair which does not support passphrase protected private keys. The most robust security protections available to the key_file are simple file system permissions.
- `root_ca_file` (String) When set the provider will use this file as the root certificate authority for the API server. This is useful if the API server is using a self-signed certificate.
- `root_ca_string` (String) When set the provider will use this string as the root certificate authority for the API server. This is useful if the API server is using a self-signed certificate.
- `oauth_client_credentials` (Block List, Max: 1) Configuration for oauth client credential flow (see [below for nested schema](#nestedblock--oauth_client_credentials))
- `password` (String) When set, will use this password for BASIC auth to the API.
- `rate_limit` (Number) Set this to limit the number of requests per second made to the API.
Expand Down
31 changes: 31 additions & 0 deletions restapi/api_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
Expand All @@ -12,6 +13,7 @@ import (
"net/http"
"net/http/cookiejar"
"net/url"
"os"
"strings"
"time"

Expand Down Expand Up @@ -48,8 +50,10 @@ type apiClientOpt struct {
oauthEndpointParams url.Values
certFile string
keyFile string
rootCAFile string
certString string
keyString string
rootCAString string
debug bool
}

Expand Down Expand Up @@ -133,6 +137,33 @@ func NewAPIClient(opt *apiClientOpt) (*APIClient, error) {
tlsConfig.Certificates = []tls.Certificate{cert}
}

// Load root CA
if opt.rootCAFile != "" || opt.rootCAString != "" {
caCertPool := x509.NewCertPool()
var rootCA []byte
var err error

if opt.rootCAFile != "" {
if opt.debug {
log.Printf("api_client.go: Reading root CA file: %s\n", opt.rootCAFile)
}
rootCA, err = os.ReadFile(opt.rootCAFile)
if err != nil {
return nil, fmt.Errorf("could not read root CA file: %v", err)
}
} else {
if opt.debug {
log.Printf("api_client.go: Using provided root CA string\n")
}
rootCA = []byte(opt.rootCAString)
}

if !caCertPool.AppendCertsFromPEM(rootCA) {
return nil, errors.New("failed to append root CA certificate")
}
tlsConfig.RootCAs = caCertPool
}

tr := &http.Transport{
TLSClientConfig: tlsConfig,
Proxy: http.ProxyFromEnvironment,
Expand Down
126 changes: 125 additions & 1 deletion restapi/api_client_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
package restapi

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"log"
"math/big"
"net"
"net/http"
"os"
"testing"
"time"
)

var apiClientServer *http.Server
var (
apiClientServer *http.Server
apiClientTLSServer *http.Server
rootCA *x509.Certificate
rootCAKey *ecdsa.PrivateKey
serverCertPEM, serverKeyPEM []byte
rootCAFilePath = "rootCA.pem"
)

func TestAPIClient(t *testing.T) {
debug := false
Expand Down Expand Up @@ -89,6 +106,42 @@ func TestAPIClient(t *testing.T) {
if debug {
log.Println("client_test.go: Done")
}

// Setup and test HTTPS client with root CA
setupAPIClientTLSServer()
defer shutdownAPIClientTLSServer()
defer os.Remove(rootCAFilePath)

httpsOpt := &apiClientOpt{
uri: "https://127.0.0.1:8443/",
insecure: false,
username: "",
password: "",
headers: make(map[string]string),
timeout: 2,
idAttribute: "id",
copyKeys: make([]string, 0),
writeReturnsObject: false,
createReturnsObject: false,
rateLimit: 1,
rootCAFile: rootCAFilePath,
debug: debug,
}
httpsClient, httpsClientErr := NewAPIClient(httpsOpt)

if httpsClientErr != nil {
t.Fatalf("client_test.go: %s", httpsClientErr)
}
if debug {
log.Printf("api_client_test.go: Testing HTTPS standard OK request\n")
}
res, err = httpsClient.sendRequest("GET", "/ok", "")
if err != nil {
t.Fatalf("client_test.go: %s", err)
}
if res != "It works!" {
t.Fatalf("client_test.go: Got back '%s' but expected 'It works!'\n", res)
}
}

func setupAPIClientServer() {
Expand Down Expand Up @@ -116,3 +169,74 @@ func setupAPIClientServer() {
func shutdownAPIClientServer() {
apiClientServer.Close()
}

func setupAPIClientTLSServer() {
generateCertificates()

cert, _ := tls.X509KeyPair(serverCertPEM, serverKeyPEM)

serverMux := http.NewServeMux()
serverMux.HandleFunc("/ok", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("It works!"))
})

apiClientTLSServer = &http.Server{
Addr: "127.0.0.1:8443",
Handler: serverMux,
TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}},
}
go apiClientTLSServer.ListenAndServeTLS("", "")
/* let the server start */
time.Sleep(1 * time.Second)
}

func shutdownAPIClientTLSServer() {
apiClientTLSServer.Close()
}

func generateCertificates() {
// Create a CA certificate and key
rootCAKey, _ = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
rootCA = &x509.Certificate{
SerialNumber: big.NewInt(2024),
Subject: pkix.Name{
Organization: []string{"Test Root CA"},
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(10, 0, 0),
IsCA: true,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
BasicConstraintsValid: true,
}
rootCABytes, _ := x509.CreateCertificate(rand.Reader, rootCA, rootCA, &rootCAKey.PublicKey, rootCAKey)

// Create a server certificate and key
serverKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
serverCert := &x509.Certificate{
SerialNumber: big.NewInt(2024),
Subject: pkix.Name{
Organization: []string{"Test Server"},
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(0, 0, 1),
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
BasicConstraintsValid: true,
}

// Add IP SANs to the server certificate
serverCert.IPAddresses = append(serverCert.IPAddresses, net.ParseIP("127.0.0.1"))

serverCertBytes, _ := x509.CreateCertificate(rand.Reader, serverCert, rootCA, &serverKey.PublicKey, rootCAKey)

// PEM encode the certificates and keys
serverCertPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: serverCertBytes})

// Marshal the server private key
serverKeyBytes, _ := x509.MarshalECPrivateKey(serverKey)
serverKeyPEM = pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: serverKeyBytes})

rootCAPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCABytes})
_ = os.WriteFile(rootCAFilePath, rootCAPEM, 0644)
}
18 changes: 18 additions & 0 deletions restapi/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,18 @@ func Provider() *schema.Provider {
DefaultFunc: schema.EnvDefaultFunc("REST_API_KEY_FILE", nil),
Description: "When set with the cert_file parameter, the provider will load a client certificate as a file for mTLS authentication. Note that this mechanism simply delegates to golang's tls.LoadX509KeyPair which does not support passphrase protected private keys. The most robust security protections available to the key_file are simple file system permissions.",
},
"root_ca_file": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("REST_API_ROOT_CA_FILE", nil),
Description: "When set, the provider will load a root CA certificate as a file for mTLS authentication. This is useful when the API server is using a self-signed certificate and the client needs to trust it.",
},
"root_ca_string": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("REST_API_ROOT_CA_STRING", nil),
Description: "When set, the provider will load a root CA certificate as a string for mTLS authentication. This is useful when the API server is using a self-signed certificate and the client needs to trust it.",
},
},
ResourcesMap: map[string]*schema.Resource{
/* Could only get terraform to recognize this resource if
Expand Down Expand Up @@ -284,7 +296,13 @@ func configureProvider(d *schema.ResourceData) (interface{}, error) {
if v, ok := d.GetOk("key_string"); ok {
opt.keyString = v.(string)
}
if v, ok := d.GetOk("root_ca_file"); ok {
opt.rootCAFile = v.(string)
}
if v, ok := d.GetOk("root_ca_string"); ok {
opt.rootCAString = v.(string)

}
client, err := NewAPIClient(opt)

if v, ok := d.GetOk("test_path"); ok {
Expand Down