Skip to content

Commit

Permalink
server: add authentication based on TLS
Browse files Browse the repository at this point in the history
* identifier: add TLSBased

This is only the identifier, the server setup still has to be done.

Note that it diverges a little from what was proposed in the issue:
not every client cert needs to have a CN record -- so instead, we'll
use whatever is the cert's subject as client identity.

* Drive-by fix: identifier_test: don't use same package for TokenBased
  tests.
* server: require and verify client cert for AuthenticationTLS
* server: allow setting CA pool via --tls-ca-cert-file
* server: expose new authentication via parameter
* [nit] server: simplify getListenerForHTTPServer
* server_test: use httptest for integration-y TLS tests
* book/security: mention TLS authn with example

Signed-off-by: Stephan Renatus <srenatus@chef.io>
  • Loading branch information
srenatus authored and tsandall committed Jan 14, 2019
1 parent 85b1931 commit 3286c39
Show file tree
Hide file tree
Showing 22 changed files with 735 additions and 14 deletions.
33 changes: 29 additions & 4 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ package cmd
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"os"
"path"

"github.com/spf13/cobra"

"github.com/open-policy-agent/opa/runtime"
"github.com/open-policy-agent/opa/server"
"github.com/open-policy-agent/opa/util"
"github.com/spf13/cobra"
)

const (
Expand All @@ -26,14 +29,14 @@ const (
func init() {

var serverMode bool
var tlsCertFile string
var tlsPrivateKeyFile string
var tlsCertFile, tlsPrivateKeyFile, tlsCACertFile string
var ignore []string

authentication := util.NewEnumFlag("off", []string{"token", "off"})
authentication := util.NewEnumFlag("off", []string{"token", "tls", "off"})

authenticationSchemes := map[string]server.AuthenticationScheme{
"token": server.AuthenticationToken,
"tls": server.AuthenticationTLS,
"off": server.AuthenticationOff,
}

Expand Down Expand Up @@ -95,6 +98,15 @@ the data document with the following syntax:
os.Exit(1)
}

if tlsCACertFile != "" {
pool, err := loadCertPool(tlsCACertFile)
if err != nil {
fmt.Println("error:", err)
os.Exit(1)
}
params.CertPool = pool
}

params.Authentication = authenticationSchemes[authentication.String()]
params.Authorization = authorizationScheme[authorization.String()]
params.Certificate = cert
Expand Down Expand Up @@ -137,6 +149,7 @@ the data document with the following syntax:
runCommand.Flags().MarkDeprecated("server-diagnostics-buffer-size", "use decision logging instead")
runCommand.Flags().StringVarP(&tlsCertFile, "tls-cert-file", "", "", "set path of TLS certificate file")
runCommand.Flags().StringVarP(&tlsPrivateKeyFile, "tls-private-key-file", "", "", "set path of TLS private key file")
runCommand.Flags().StringVarP(&tlsCACertFile, "tls-ca-cert-file", "", "", "set path of TLS CA cert file")
runCommand.Flags().VarP(authentication, "authentication", "", "set authentication scheme")
runCommand.Flags().VarP(authorization, "authorization", "", "set authorization scheme")
runCommand.Flags().VarP(logLevel, "log-level", "l", "set log level")
Expand Down Expand Up @@ -177,3 +190,15 @@ func loadCertificate(tlsCertFile, tlsPrivateKeyFile string) (*tls.Certificate, e

return nil, nil
}

func loadCertPool(tlsCACertFile string) (*x509.CertPool, error) {
caCertPEM, err := ioutil.ReadFile(tlsCACertFile)
if err != nil {
return nil, fmt.Errorf("read CA cert file: %v", err)
}
pool := x509.NewCertPool()
if ok := pool.AppendCertsFromPEM(caCertPEM); !ok {
return nil, fmt.Errorf("failed to parse CA cert %q", tlsCACertFile)
}
return pool, nil
}
144 changes: 144 additions & 0 deletions docs/book/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ startup:
OPA will exit immediately with a non-zero status code if only one of these flags
is specified.

Note that for using TLS-based authentication, a CA cert file can be provided:

- ``--tls-ca-cert-file=<path>`` specifies the path of the file containing the CA cert.

If provided, it will be used to validate clients' TLS certificates when using TLS
authentication (see below).

By default, OPA ignores insecure HTTP connections when TLS is enabled. To allow
insecure HTTP connections in addition to HTTPS connections, provide another
listening address with `--addr`. For example:
Expand Down Expand Up @@ -98,6 +105,17 @@ and provide to the authorization handler. When you use the `token`
authentication, you must configure an authorization policy that checks the
tokens. If the client does not supply a Bearer token, the `input.identity`
value will be undefined when the authorization policy is evaluated.
- Client TLS certificates: Client TLS authentication is enabled by starting
OPA with ``--authentication=tls``. When this authentication mode is enabled,
OPA will require all clients to provide a client certificate. It is verified
against the CA certificate(s) provided via `--tls-ca-cert-path`. Upon successful
verification, the `input.identity` value is set to the TLS certificate's
subject.

Note that TLS authentication does not disable non-HTTPS listeners. To ensure
that all your communication is secured, it should be paired with an
authorization policy (see below) that at least requires the client identity
(`input.identity`) to _be set_.

For authorization, OPA relies on policy written in Rego. Authorization is
enabled by starting OPA with ``--authorization=basic``.
Expand Down Expand Up @@ -198,6 +216,8 @@ HTTP/1.1 200 OK
Content-Type: application/json
```

### Token-based Authentication Example

When Bearer tokens are used for authentication, the policy should at minimum
validate the identity:

Expand Down Expand Up @@ -276,3 +296,127 @@ identity_rights[right] { # Right is in the identity_rights set if...
right = rights[role] # Role has rights defined.
}
```

### TLS-based Authentication Example

To set up authentication based on TLS, we will need three certificates:

1. the CA cert (self-signed),
2. the server cert (signed by the CA), and
3. the client cert (signed by the CA).

These are example invocations using `openssl`.
Don't use these in production, the key sizes are only good for demonstration purposes.

Note that we're creating an extra client, which has a certificate signed by the proper
CA, but will later be used to illustrate the authorization policy.

```bash
# CA
openssl genrsa -out ca-key.pem 2048
openssl req -x509 -new -nodes -key ca-key.pem -days 1000 -out ca.pem -subj "/CN=my-ca"

# client 1
openssl genrsa -out client-key.pem 2048
openssl req -new -key client-key.pem -out csr.pem -subj "/CN=my-client"
openssl x509 -req -in csr.pem -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem -days 1000

# client 2
openssl genrsa -out client-key-2.pem 2048
openssl req -new -key client-key-2.pem -out csr.pem -subj "/CN=my-client-2"
openssl x509 -req -in csr.pem -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out client-cert-2.pem -days 1000

# create server cert with IP and DNS SANs
cat <<EOF >req.cnf
[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
[req_distinguished_name]
[v3_req]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = opa.example.com
IP.1 = 127.0.0.1
EOF
openssl genrsa -out server-key.pem 2048
openssl req -new -key server-key.pem -out csr.pem -subj "/CN=my-server" -config req.cnf
openssl x509 -req -in csr.pem -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -days 1000 -extensions v3_req -extfile req.cnf
```

We also create a simple authorization policy file, called `check.rego`:
```ruby
package system.authz

# client_cns may defined in policy or pushed into OPA as data.
client_cns = {
"my-client": true
}

default allow = false

allow { # Allow request if
split(input.identity, "=", ["CN", cn]) # the cert subject is a CN, and
client_cns[cn] # the name is a known client.
}
```

Now, we're ready to starting the server with `-authentication=tls` and the
certificate-related parameters:
```console
$ opa run -s \
--tls-cert-file server-cert.pem \
--tls-private-key-file server-key.pem \
--tls-ca-cert-file ca.pem \
--authentication=tls \
--authorization=basic \
-a https://127.0.0.1:8181 \
check.rego
INFO[2019-01-14T10:24:52+01:00] First line of log stream. addrs="[https://127.0.0.1:8181]" insecure_addr=
```

We can use `curl` to validate our TLS-based authentication setup:

First, we use the client certificate that was signed by the CA, and has a subject
matching our authorization policy:

```console
$ curl --key client-key.pem \
--cert client-cert.pem \
--cacert ca.pem \
--resolve opa.example.com:8181:127.0.0.1 \
https://opa.example.com:8181/v1/data
{"result":{}}
```

Note that we're passing the CA cert to curl -- this is done to have curl accept
the server's certificate, which has been signed by our CA cert.

Since we've setup an IP SAN, we may also `curl https://127.0.0.1:8181/v1/data`
directly. (To keep our examples focused, we'll do that from here on.)

Using a valid certificate whose subject will be declined by our authorization
policy:

```console
$ curl --key client-key-2.pem \
--cert client-cert-2.pem \
--cacert ca.pem \
https://127.0.0.1:8181/v1/data
{
"code": "unauthorized",
"message": "request rejected by administrative policy"
}
```

Finally, we'll attempt to query without a client certificate:
```console
$ curl --cacert ca.pem https://127.0.0.1:8181/v1/data
curl: (35) error:14094412:SSL routines:ssl3_read_bytes:sslv3 alert bad certificate
```

As you can see, TLS-based authentication disallows these request completely.
5 changes: 5 additions & 0 deletions runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"context"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"io/ioutil"
Expand Down Expand Up @@ -71,6 +72,9 @@ type Params struct {
// is nil, the server will NOT use TLS.
Certificate *tls.Certificate

// CertPool holds the CA certs trusted by the OPA server.
CertPool *x509.CertPool

// HistoryPath is the filename to store the interactive shell user
// input history.
HistoryPath string
Expand Down Expand Up @@ -235,6 +239,7 @@ func (rt *Runtime) StartServer(ctx context.Context) {
WithAddresses(*rt.Params.Addrs).
WithInsecureAddress(rt.Params.InsecureAddr).
WithCertificate(rt.Params.Certificate).
WithCertPool(rt.Params.CertPool).
WithAuthentication(rt.Params.Authentication).
WithAuthorization(rt.Params.Authorization).
WithDiagnosticsBuffer(rt.Params.DiagnosticsBuffer).
Expand Down
8 changes: 5 additions & 3 deletions server/identifier/identifier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.

package identifier
package identifier_test

import (
"net/http"
"testing"

"github.com/open-policy-agent/opa/server/identifier"
)

type mockHandler struct {
Expand All @@ -15,13 +17,13 @@ type mockHandler struct {
}

func (h *mockHandler) ServeHTTP(_ http.ResponseWriter, r *http.Request) {
h.identity, h.defined = Identity(r)
h.identity, h.defined = identifier.Identity(r)
}

func TestTokenBased(t *testing.T) {

mock := &mockHandler{}
handler := NewTokenBased(mock)
handler := identifier.NewTokenBased(mock)

req, err := http.NewRequest(http.MethodGet, "/foo/bar/baz", nil)
if err != nil {
Expand Down
5 changes: 5 additions & 0 deletions server/identifier/testdata/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
*.srl
*.cnf
csr.pem
ca-key.pem
ca.pem
18 changes: 18 additions & 0 deletions server/identifier/testdata/cn-cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE-----
MIIC5DCCAcygAwIBAgIJAJEUtRCBCdAGMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV
BAMMBW15LWNhMB4XDTE5MDExMTA5MjM1NFoXDTIxMTAwNzA5MjM1NFowFDESMBAG
A1UEAwwJbXktY2xpZW50MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
2niMJJAlwthZjF2hT8IoaQ1FwZ1DyDPh0HBWRNQxAW0yNZVaeLUEwt9U5FkZiusr
bKDI0jD0MOVWQw1s8oj3ys7Zxei9B8c0KBhnXDbkKdFKs5uzgUMnJyyAwjRi8T3P
tyQtKK6phTBwseBEAXleoLYTUWAE9Q6W1wf9PHiY6Rak1HEUOU2AoV5gmZd8odKo
gn+5bmgm6so6BXty7DQod4J2tnq8/TC/1HM7TN+N0eaLtyjhmbU9TA/C7yeil4Fm
i3Sk+P/5CprLnPEGN3fXs5RAMk7PxHR+NhEIWMeNKzAUDMQZ662cWAJRe5L6l8aB
O4zN7vzcJLyiHfZMJEqzSQIDAQABoz0wOzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIF
4DAhBgNVHREEGjAYghZjbGllbnQub3BhLmV4YW1wbGUuY29tMA0GCSqGSIb3DQEB
CwUAA4IBAQAYtVMyy799xyAbMzn0EuoFmrNaiuCWkNC4NV6I7CotEnBJqeCIR7LU
VxHOGPLeXlLki1rDx1elTNY3HuSSEyCTSAQc9thhVoBlnndHnTF+sTEiFBAZxrjw
+j7Kdh9AEAMOTAl/CnU8mziIDaoGLDEckpi32QzgGht4yIetNR85R6Y1J4RhuEI9
cNMu5hMbEhL9L6wcIWEn9w9Y6bYqBHPWrprUG9AjTteP2CRadqYiHmbo8FG6Sor/
jGIChe39L/fL6mG8bT4ageZdTWlA6fr+jcbnYFG+zhT9KhmUVlo7G7OMvgq81lsF
nCDsGTDE/U424T0s8uWw8P/WmTXJihr/
-----END CERTIFICATE-----
29 changes: 29 additions & 0 deletions server/identifier/testdata/gencerts.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/bin/bash
# taken from
# https://github.com/dexidp/dex/blob/2d1ac74ec0ca12ae4d36072525d976c1a596820a/examples/k8s/gencert.sh#L22

cat <<EOF >req.cnf
[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
[req_distinguished_name]
[v3_req]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = client.opa.example.com
EOF

openssl genrsa -out ca-key.pem 2048
openssl req -x509 -new -nodes -key ca-key.pem -days 1000 -out ca.pem -subj "/CN=my-ca"

openssl genrsa -out key.pem 2048
openssl req -new -key key.pem -out csr.pem -subj "/CN=my-client" -config req.cnf
openssl x509 -req -in csr.pem -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out cn-cert.pem -days 1000 -extensions v3_req -extfile req.cnf

openssl req -new -key key.pem -out csr.pem -subj "/O=Torchwood/OU=opa-client-01" -config req.cnf
openssl x509 -req -in csr.pem -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out ou-cert.pem -days 1000 -extensions v3_req -extfile req.cnf
27 changes: 27 additions & 0 deletions server/identifier/testdata/key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA2niMJJAlwthZjF2hT8IoaQ1FwZ1DyDPh0HBWRNQxAW0yNZVa
eLUEwt9U5FkZiusrbKDI0jD0MOVWQw1s8oj3ys7Zxei9B8c0KBhnXDbkKdFKs5uz
gUMnJyyAwjRi8T3PtyQtKK6phTBwseBEAXleoLYTUWAE9Q6W1wf9PHiY6Rak1HEU
OU2AoV5gmZd8odKogn+5bmgm6so6BXty7DQod4J2tnq8/TC/1HM7TN+N0eaLtyjh
mbU9TA/C7yeil4Fmi3Sk+P/5CprLnPEGN3fXs5RAMk7PxHR+NhEIWMeNKzAUDMQZ
662cWAJRe5L6l8aBO4zN7vzcJLyiHfZMJEqzSQIDAQABAoIBAQDBQl4GghVFVYlx
p+no2kJRG9KXQX0SfwLFFnraDDMFpgkCaYpMuSTrFhDMpxz3TK1vPJQpi/CXyGgU
jK3RpuQ8Xds7PXTqiodS6LOWWWBgtam1VIjoUfUyrCWCpkDYUuuKgNAJ6ug+z+kB
EPhXrXvOAwL3u07nUO6SbZjQg4YQubpT7znOzAaqE+33sCDsP/tdvliYdm18udvw
0TCTGUwEPGVfdYsSO8iHDikmaIFAjepd+w+PytsdCpzb7YjThHJu0SzvA3DozBWO
V0OpEX2tst48/HVYv7UrLeJXdizaa37o1v8Bxsai0p9WW9PY2376EglxPTf7EhTp
ANjrEFTZAoGBAPg3D2VBgETawknZHYOH+Zh0gTEEEOZkC+cJmgAVvVdjfIJXvJ8u
Rfrb13hCR2VS6xoymN6w8FTS1UTVBBV6IXssrFinWwuNQaoa3qrkmlNVl2nRQxvq
3OmqyckWXNqwBWL+Hhn0sfhD+b55C9evrcW2tf/6qj9w6r1qPle4D0NTAoGBAOFS
qy5ECUyZddJTpRhEDMOneFqMsymD28G6fuitACaxeqOAEaAMTSZDuf3JS6JkpnQS
EqrR1PEzFu2rKzRw/rX4GYplH/7gvimGd1hvLtftDCSMOFXwVmdgKHZjiqg74Nzl
QWH7kUfjNQgTldTFWzDLfuyIStXAv76f24UOsRdzAoGAaXyc4l9v79M4dsH6tQd4
n74DmZ0swX0LQejmtdqHWThClfJLiyrTOsVrUQR56ynOGJggN6Piv2nKkTImRipd
SEe4BwU4wDQMEArTTrVQkNHzQ1lXt+mccQHQN9F1LMtZvrRYfpdreyMIZFZ1Hfjf
VQNNXbhd2hBW8qDQVd83PVkCgYAh9Gc/bZlJJccPjvNOGNMjmNUWMCW/l9NB+myt
e4SOUCh/Awmk6LWnkoUwrWjsa+Z5j0+o1j4UqvJFlonIOU7o9R5EMMEFk7CUaWMK
vJZ+i4ZM66SBrtoWcfMnBBEdEQjtwM59iX93KdIQCYOGsMbxL3lNA6zjUUyT2Vsn
TfN56QKBgAEGdgdbiidvZiifEKtrE7/dEOJvGa/d8zTAfyURQwqOSSN1MhECLyo4
NBHjcH2AOW+lmwwCv5MyCX9VX8hUIVUllODcnU4ssMmliZmi6i6bqW+/zJ+w3p+F
RM3yPNTMHhM6IBgmXam7ZApN4NLdU4O5oZsLPS8tTQIWObP/M/nd
-----END RSA PRIVATE KEY-----
Loading

0 comments on commit 3286c39

Please sign in to comment.