Skip to content

Commit

Permalink
topdown: support inline TLS certificates and keys
Browse files Browse the repository at this point in the history
`http.send` already supports TLS keys and certificates obtained from
external files and environment variables. Add support for specifying
certificates and keys as direct parameters to `http.send`. This is
useful when you already have policy variables with this data.

There are 3 options that this affects: client certificates, client
keys and CA bundles. All of these can now be specified from either a
file, an environment variable or from raw data. In the case of client
certificates and keys, each access method must use a matched pair (e.g.
both the certificate and the key must come from environment variables).
CA certificate bundles are the union of all the specified access methods.

Signed-off-by: James Peach <jpeach@vmware.com>
  • Loading branch information
jpeach authored and patrick-east committed Apr 9, 2020
1 parent 4c81aa7 commit ce92d19
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 62 deletions.
12 changes: 8 additions & 4 deletions docs/content/policy-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -565,13 +565,16 @@ The `request` object parameter may contain the following fields:
| `headers` | no | `object` | HTTP headers to include in the request (e.g,. `{"X-Opa": "rules"}`). |
| `enable_redirect` | no | `boolean` | Follow HTTP redirects. Default: `false`. |
| `force_json_decode` | no | `boolean` | Decode the HTTP response message body as JSON even if the `Content-Type` header is missing. Default: `false`. |
| `tls_use_system_certs` | no | `boolean` | Use system certificate pool. |
| `tls_use_system_certs` | no | `boolean` | Use the system certificate pool. Default: `false`. |
| `tls_ca_cert` | no | `string` | String containing a root certificate in PEM encoded format. |
| `tls_ca_cert_file` | no | `string` | Path to file containing a root certificate in PEM encoded format. |
| `tls_ca_cert_env_variable` | no | `string` | Environment variable containing a root certificate in PEM encoded format. |
| `tls_client_cert` | no | `string` | String containing a client certificate in PEM encoded format. |
| `tls_client_cert_file` | no | `string` | Path to file containing a client certificate in PEM encoded format. |
| `tls_client_cert_env_variable` | no | `string` | Environment variable containing a client certificate in PEM encoded format. |
| `tls_client_key` | no | `string` | String containing a key in PEM encoded format. |
| `tls_client_key_file` | no | `string` | Path to file containing a key in PEM encoded format. |
| `tls_client_key_env_variable` | no | `string` | Environment variable containing a client key in PEM encoded format. |
| `tls_client_cert_file` | no | `string` | Path to file containing a client certificate in PEM encoded format. |
| `tls_client_key_file` | no | `string` | Path to file containing a key in PEM encoded format. |
| `timeout` | no | `string` or `number` | Timeout for the HTTP request with a default of 5 seconds (`5s`). Numbers provided are in nanoseconds. Strings must be a valid duration string where a duration string is a possibly signed sequence of decimal numbers, each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". A zero timeout means no timeout.|
| `tls_insecure_skip_verify` | no | `bool` | Allows for skipping TLS verification when calling a network endpoint. Not recommended for production. |
| `tls_server_name` | no | `string` | Sets the hostname that is sent in the client Server Name Indication and that be will be used for server certificate validation. If this is not set, the value of the `Host` header (if present) will be used. If neither are set, the host name from the requested URL is used. |
Expand All @@ -580,10 +583,11 @@ If the `Host` header is included in `headers`, its value will be used as the `Ho

When sending HTTPS requests with client certificates at least one the following combinations must be included

* ``tls_client_cert`` and ``tls_client_key``
* ``tls_client_cert_file`` and ``tls_client_key_file``
* ``tls_client_cert_env_variable`` and ``tls_client_key_env_variable``

> The user must also provide a trusted root CA through tls_ca_cert_file or tls_ca_cert_env_variable. Alternatively the user could set tls_use_system_certs to ``true`` and the system certificate pool will be used.
> To validate TLS server certificates, the user must also provide trusted root CA certificates through the ``tls_ca_cert``, ``tls_ca_cert_file`` and ``tls_ca_cert_env_variable`` fields. If the ``tls_use_system_certs`` field is ``true``, the system certificate pool will be used as well as any additional CA certificates.
The `response` object parameter will contain the following fields:

Expand Down
62 changes: 36 additions & 26 deletions topdown/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
"os"

"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/topdown/builtins"
Expand Down Expand Up @@ -71,40 +72,49 @@ func init() {
RegisterFunctionalBuiltin1(ast.CryptoSha256.Name, builtinCryptoSha256)
}

// createRootCAs creates a new Cert Pool from scratch or adds to a copy of System Certs
func createRootCAs(tlsCACertFile string, tlsCACertEnvVar []byte, tlsUseSystemCerts bool) (*x509.CertPool, error) {
// addCACertsFromFile adds CA certificates from filePath into the given pool.
// If pool is nil, it creates a new x509.CertPool. pool is returned.
func addCACertsFromFile(pool *x509.CertPool, filePath string) (*x509.CertPool, error) {
if pool == nil {
pool = x509.NewCertPool()
}

var newRootCAs *x509.CertPool
caCert, err := readCertFromFile(filePath)
if err != nil {
return nil, err
}

if tlsUseSystemCerts {
systemCertPool, err := x509.SystemCertPool()
if err != nil {
return nil, err
}
newRootCAs = systemCertPool
} else {
newRootCAs = x509.NewCertPool()
if ok := pool.AppendCertsFromPEM(caCert); !ok {
return nil, fmt.Errorf("could not append CA certificates from %q", filePath)
}

if len(tlsCACertFile) > 0 {
// Append our cert to the system pool
caCert, err := readCertFromFile(tlsCACertFile)
if err != nil {
return nil, err
}
if ok := newRootCAs.AppendCertsFromPEM(caCert); !ok {
return nil, fmt.Errorf("could not append CA cert from %q", tlsCACertFile)
}
return pool, nil
}

// addCACertsFromBytes adds CA certificates from pemBytes into the given pool.
// If pool is nil, it creates a new x509.CertPool. pool is returned.
func addCACertsFromBytes(pool *x509.CertPool, pemBytes []byte) (*x509.CertPool, error) {
if pool == nil {
pool = x509.NewCertPool()
}

if len(tlsCACertEnvVar) > 0 {
// Append our cert to the system pool
if ok := newRootCAs.AppendCertsFromPEM(tlsCACertEnvVar); !ok {
return nil, fmt.Errorf("error appending cert from env var %q into system certs", tlsCACertEnvVar)
}
if ok := pool.AppendCertsFromPEM(pemBytes); !ok {
return nil, fmt.Errorf("could not append certificates")
}

return pool, nil
}

// addCACertsFromBytes adds CA certificates from the environment variable named
// by envName into the given pool. If pool is nil, it creates a new x509.CertPool.
// pool is returned.
func addCACertsFromEnv(pool *x509.CertPool, envName string) (*x509.CertPool, error) {
pool, err := addCACertsFromBytes(pool, []byte(os.Getenv(envName)))
if err != nil {
return nil, fmt.Errorf("could not add CA certificates from envvar %q: %w", envName, err)
}

return newRootCAs, nil
return pool, err
}

// ReadCertFromFile reads a cert from file
Expand Down
130 changes: 98 additions & 32 deletions topdown/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,20 @@ package topdown
import (
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/url"
"strconv"

"github.com/open-policy-agent/opa/internal/version"

"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"

"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/internal/version"
"github.com/open-policy-agent/opa/topdown/builtins"
)

Expand All @@ -38,12 +37,15 @@ var allowedKeyNames = [...]string{
"headers",
"raw_body",
"tls_use_system_certs",
"tls_ca_cert",
"tls_ca_cert_file",
"tls_ca_cert_env_variable",
"tls_client_cert_env_variable",
"tls_client_key_env_variable",
"tls_client_cert",
"tls_client_cert_file",
"tls_client_cert_env_variable",
"tls_client_key",
"tls_client_key_file",
"tls_client_key_env_variable",
"tls_insecure_skip_verify",
"tls_server_name",
"timeout",
Expand Down Expand Up @@ -150,20 +152,30 @@ func canonicalizeHeaders(headers map[string]interface{}) map[string]interface{}
func executeHTTPRequest(bctx BuiltinContext, obj ast.Object) (ast.Value, error) {
var url string
var method string
var tlsCaCertEnvVar []byte

// Additional CA certificates loading options.
var tlsCaCert []byte
var tlsCaCertEnvVar string
var tlsCaCertFile string
var tlsClientKeyEnvVar []byte
var tlsClientCertEnvVar []byte

// Client TLS certificate and key options. Each input source
// comes in a matched pair.
var tlsClientCert []byte
var tlsClientKey []byte

var tlsClientCertEnvVar string
var tlsClientKeyEnvVar string

var tlsClientCertFile string
var tlsClientKeyFile string

var tlsServerName string
var body *bytes.Buffer
var rawBody *bytes.Buffer
var enableRedirect bool
var forceJSONDecode bool
var tlsUseSystemCerts bool
var tlsConfig tls.Config
var clientCerts []tls.Certificate
var customHeaders map[string]interface{}
var tlsInsecureSkipVerify bool
var timeout = defaultHTTPRequestTimeout
Expand Down Expand Up @@ -215,27 +227,33 @@ func executeHTTPRequest(bctx BuiltinContext, obj ast.Object) (ast.Value, error)
if err != nil {
return nil, err
}
case "tls_ca_cert":
tlsCaCert = []byte(obj.Get(val).String())
tlsCaCert = bytes.Trim(tlsCaCert, "\"")
case "tls_ca_cert_file":
tlsCaCertFile = obj.Get(val).String()
tlsCaCertFile = strings.Trim(tlsCaCertFile, "\"")
case "tls_ca_cert_env_variable":
caCertEnv := obj.Get(val).String()
caCertEnv = strings.Trim(caCertEnv, "\"")
tlsCaCertEnvVar = []byte(os.Getenv(caCertEnv))
case "tls_client_cert_env_variable":
clientCertEnv := obj.Get(val).String()
clientCertEnv = strings.Trim(clientCertEnv, "\"")
tlsClientCertEnvVar = []byte(os.Getenv(clientCertEnv))
case "tls_client_key_env_variable":
clientKeyEnv := obj.Get(val).String()
clientKeyEnv = strings.Trim(clientKeyEnv, "\"")
tlsClientKeyEnvVar = []byte(os.Getenv(clientKeyEnv))
tlsCaCertEnvVar = obj.Get(val).String()
tlsCaCertEnvVar = strings.Trim(tlsCaCertEnvVar, "\"")
case "tls_client_cert":
tlsClientCert = []byte(obj.Get(val).String())
tlsClientCert = bytes.Trim(tlsClientCert, "\"")
case "tls_client_cert_file":
tlsClientCertFile = obj.Get(val).String()
tlsClientCertFile = strings.Trim(tlsClientCertFile, "\"")
case "tls_client_cert_env_variable":
tlsClientCertEnvVar = obj.Get(val).String()
tlsClientCertEnvVar = strings.Trim(tlsClientCertEnvVar, "\"")
case "tls_client_key":
tlsClientKey = []byte(obj.Get(val).String())
tlsClientKey = bytes.Trim(tlsClientKey, "\"")
case "tls_client_key_file":
tlsClientKeyFile = obj.Get(val).String()
tlsClientKeyFile = strings.Trim(tlsClientKeyFile, "\"")
case "tls_client_key_env_variable":
tlsClientKeyEnvVar = obj.Get(val).String()
tlsClientKeyEnvVar = strings.Trim(tlsClientKeyEnvVar, "\"")
case "tls_server_name":
tlsServerName = obj.Get(val).String()
tlsServerName = strings.Trim(tlsServerName, "\"")
Expand Down Expand Up @@ -275,34 +293,82 @@ func executeHTTPRequest(bctx BuiltinContext, obj ast.Object) (ast.Value, error)
tlsConfig.InsecureSkipVerify = tlsInsecureSkipVerify
}

if len(tlsClientCert) > 0 && len(tlsClientKey) > 0 {
tlsClientCert = bytes.Replace(tlsClientCert, []byte("\\n"), []byte("\n"), -1)
tlsClientKey = bytes.Replace(tlsClientKey, []byte("\\n"), []byte("\n"), -1)
cert, err := tls.X509KeyPair(tlsClientCert, tlsClientKey)
if err != nil {
return nil, err
}

isTLS = true
tlsConfig.Certificates = append(tlsConfig.Certificates, cert)
}

if tlsClientCertFile != "" && tlsClientKeyFile != "" {
clientCertFromFile, err := tls.LoadX509KeyPair(tlsClientCertFile, tlsClientKeyFile)
cert, err := tls.LoadX509KeyPair(tlsClientCertFile, tlsClientKeyFile)
if err != nil {
return nil, err
}
clientCerts = append(clientCerts, clientCertFromFile)

isTLS = true
tlsConfig.Certificates = append(tlsConfig.Certificates, cert)
}

if len(tlsClientCertEnvVar) > 0 && len(tlsClientKeyEnvVar) > 0 {
clientCertFromEnv, err := tls.X509KeyPair(tlsClientCertEnvVar, tlsClientKeyEnvVar)
if tlsClientCertEnvVar != "" && tlsClientKeyEnvVar != "" {
cert, err := tls.X509KeyPair(
[]byte(os.Getenv(tlsClientCertEnvVar)),
[]byte(os.Getenv(tlsClientKeyEnvVar)))
if err != nil {
return nil, fmt.Errorf("cannot extract public/private key pair from envvars %q, %q: %w",
tlsClientCertEnvVar, tlsClientKeyEnvVar, err)
}

isTLS = true
tlsConfig.Certificates = append(tlsConfig.Certificates, cert)
}

// Check the system certificates config first so that we
// load additional certificated into the correct pool.
if tlsUseSystemCerts {
pool, err := x509.SystemCertPool()
if err != nil {
return nil, err
}
clientCerts = append(clientCerts, clientCertFromEnv)

isTLS = true
tlsConfig.RootCAs = pool
}

if len(clientCerts) > 0 {
if len(tlsCaCert) != 0 {
tlsCaCert = bytes.Replace(tlsCaCert, []byte("\\n"), []byte("\n"), -1)
pool, err := addCACertsFromBytes(tlsConfig.RootCAs, []byte(tlsCaCert))
if err != nil {
return nil, err
}

isTLS = true
tlsConfig.Certificates = append(tlsConfig.Certificates, clientCerts...)
tlsConfig.RootCAs = pool
}

if tlsUseSystemCerts || len(tlsCaCertFile) > 0 || len(tlsCaCertEnvVar) > 0 {
if tlsCaCertFile != "" {
pool, err := addCACertsFromFile(tlsConfig.RootCAs, tlsCaCertFile)
if err != nil {
return nil, err
}

isTLS = true
connRootCAs, err := createRootCAs(tlsCaCertFile, tlsCaCertEnvVar, tlsUseSystemCerts)
tlsConfig.RootCAs = pool
}

if tlsCaCertEnvVar != "" {
pool, err := addCACertsFromEnv(tlsConfig.RootCAs, tlsCaCertEnvVar)
if err != nil {
return nil, err
}
tlsConfig.RootCAs = connRootCAs

isTLS = true
tlsConfig.RootCAs = pool
}

if isTLS {
Expand Down
Loading

0 comments on commit ce92d19

Please sign in to comment.