mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-02 03:27:54 +00:00
credential/cert: First pass at public key credential backend
This commit is contained in:
109
builtin/credential/cert/backend.go
Normal file
109
builtin/credential/cert/backend.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package cert
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
|
||||
func Factory(map[string]string) (logical.Backend, error) {
|
||||
return Backend(), nil
|
||||
}
|
||||
|
||||
func Backend() *framework.Backend {
|
||||
var b backend
|
||||
b.MapCertId = &framework.PolicyMap{
|
||||
PathMap: framework.PathMap{
|
||||
Name: "ca",
|
||||
Schema: map[string]*framework.FieldSchema{
|
||||
"certificate": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: "The public certificate that should be trusted. Must be x509 PEM encoded.",
|
||||
},
|
||||
|
||||
"display_name": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: "The display name to use for clients using this certificate",
|
||||
},
|
||||
|
||||
"value": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: "Policies for the certificate.",
|
||||
},
|
||||
},
|
||||
},
|
||||
DefaultKey: "default",
|
||||
}
|
||||
b.Backend = &framework.Backend{
|
||||
Help: backendHelp,
|
||||
|
||||
PathsSpecial: &logical.Paths{
|
||||
Unauthenticated: []string{
|
||||
"login",
|
||||
},
|
||||
},
|
||||
|
||||
Paths: framework.PathAppend([]*framework.Path{
|
||||
pathLogin(&b),
|
||||
},
|
||||
b.MapCertId.Paths(),
|
||||
),
|
||||
}
|
||||
|
||||
return b.Backend
|
||||
}
|
||||
|
||||
type backend struct {
|
||||
*framework.Backend
|
||||
|
||||
MapCertId *framework.PolicyMap
|
||||
}
|
||||
|
||||
const backendHelp = `
|
||||
The App ID credential provider is used to perform authentication from
|
||||
within applications or machine by pairing together two hard-to-guess
|
||||
unique pieces of information: a unique app ID, and a unique user ID.
|
||||
|
||||
The goal of this credential provider is to allow elastic users
|
||||
(dynamic machines, containers, etc.) to authenticate with Vault without
|
||||
having to store passwords outside of Vault. It is a single method of
|
||||
solving the chicken-and-egg problem of setting up Vault access on a machine.
|
||||
With this provider, nobody except the machine itself has access to both
|
||||
pieces of information necessary to authenticate. For example:
|
||||
configuration management will have the app IDs, but the machine itself
|
||||
will detect its user ID based on some unique machine property such as a
|
||||
MAC address (or a hash of it with some salt).
|
||||
|
||||
An example, real world process for using this provider:
|
||||
|
||||
1. Create unique app IDs (UUIDs work well) and map them to policies.
|
||||
(Path: map/app-id/<app-id>)
|
||||
|
||||
2. Store the app IDs within configuration management systems.
|
||||
|
||||
3. An out-of-band process run by security operators map unique user IDs
|
||||
to these app IDs. Example: when an instance is launched, a cloud-init
|
||||
system tells security operators a unique ID for this machine. This
|
||||
process can be scripted, but the key is that it is out-of-band and
|
||||
out of reach of configuration management.
|
||||
(Path: map/user-id/<user-id>)
|
||||
|
||||
4. A new server is provisioned. Configuration management configures the
|
||||
app ID, the server itself detects its user ID. With both of these
|
||||
pieces of information, Vault can be accessed according to the policy
|
||||
set by the app ID.
|
||||
|
||||
More details on this process follow:
|
||||
|
||||
The app ID is a unique ID that maps to a set of policies. This ID is
|
||||
generated by an operator and configured into the backend. The ID itself
|
||||
is usually a UUID, but any hard-to-guess unique value can be used.
|
||||
|
||||
After creating app IDs, an operator authorizes a fixed set of user IDs
|
||||
with each app ID. When the valid {app ID, user ID} set is tuple is given
|
||||
to the "login" path, then the user is authenticated with the configured
|
||||
app ID policies.
|
||||
|
||||
The user ID can be any value (just like the app ID), however it is
|
||||
generally a value unique to a machine, such as a MAC address or instance ID,
|
||||
or a value hashed from these unique values.
|
||||
`
|
||||
138
builtin/credential/cert/backend_test.go
Normal file
138
builtin/credential/cert/backend_test.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package cert
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/vault/logical"
|
||||
logicaltest "github.com/hashicorp/vault/logical/testing"
|
||||
)
|
||||
|
||||
// Test a client trusted by a CA
|
||||
func TestBackend_basic_CA(t *testing.T) {
|
||||
connState := testConnState(t, "../../../test/key/ourdomain.cer",
|
||||
"../../../test/key/ourdomain.key")
|
||||
ca, err := ioutil.ReadFile("../../../test/ca/root.cer")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
logicaltest.Test(t, logicaltest.TestCase{
|
||||
Backend: Backend(),
|
||||
Steps: []logicaltest.TestStep{
|
||||
testAccStepCert(t, "web", ca, "foo"),
|
||||
testAccStepLogin(t, connState),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Test a self-signed client that is trusted
|
||||
func TestBackend_basic_singleCert(t *testing.T) {
|
||||
connState := testConnState(t, "../../../test/unsigned/cert.pem",
|
||||
"../../../test/unsigned/key.pem")
|
||||
ca, err := ioutil.ReadFile("../../../test/unsigned/cert.pem")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
logicaltest.Test(t, logicaltest.TestCase{
|
||||
Backend: Backend(),
|
||||
Steps: []logicaltest.TestStep{
|
||||
testAccStepCert(t, "web", ca, "foo"),
|
||||
testAccStepLogin(t, connState),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Test an untrusted self-signed client
|
||||
func TestBackend_untrusted(t *testing.T) {
|
||||
connState := testConnState(t, "../../../test/unsigned/cert.pem",
|
||||
"../../../test/unsigned/key.pem")
|
||||
logicaltest.Test(t, logicaltest.TestCase{
|
||||
Backend: Backend(),
|
||||
Steps: []logicaltest.TestStep{
|
||||
testAccStepLoginInvalid(t, connState),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func testAccStepLogin(t *testing.T, connState tls.ConnectionState) logicaltest.TestStep {
|
||||
return logicaltest.TestStep{
|
||||
Operation: logical.WriteOperation,
|
||||
Path: "login",
|
||||
Unauthenticated: true,
|
||||
ConnState: &connState,
|
||||
Check: logicaltest.TestCheckAuth([]string{"foo"}),
|
||||
}
|
||||
}
|
||||
|
||||
func testAccStepLoginInvalid(t *testing.T, connState tls.ConnectionState) logicaltest.TestStep {
|
||||
return logicaltest.TestStep{
|
||||
Operation: logical.WriteOperation,
|
||||
Path: "login",
|
||||
Unauthenticated: true,
|
||||
ConnState: &connState,
|
||||
Check: func(resp *logical.Response) error {
|
||||
if resp != nil {
|
||||
return fmt.Errorf("should not be authorized")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func testAccStepCert(
|
||||
t *testing.T, name string, cert []byte, policies string) logicaltest.TestStep {
|
||||
return logicaltest.TestStep{
|
||||
Operation: logical.WriteOperation,
|
||||
Path: "map/ca/" + name,
|
||||
Data: map[string]interface{}{
|
||||
"certificate": string(cert),
|
||||
"value": policies,
|
||||
"display_name": name,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func testConnState(t *testing.T, certPath, keyPath string) tls.ConnectionState {
|
||||
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
conf := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
ClientAuth: tls.RequestClientCert,
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
list, err := tls.Listen("tcp", "127.0.0.1:0", conf)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
defer list.Close()
|
||||
|
||||
go func() {
|
||||
addr := list.Addr().String()
|
||||
conn, err := tls.Dial("tcp", addr, conf)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Write ping
|
||||
conn.Write([]byte("ping"))
|
||||
}()
|
||||
|
||||
serverConn, err := list.Accept()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
defer serverConn.Close()
|
||||
|
||||
// Read the pign
|
||||
buf := make([]byte, 4)
|
||||
serverConn.Read(buf)
|
||||
|
||||
// Grab the current state
|
||||
connState := serverConn.(*tls.Conn).ConnectionState()
|
||||
return connState
|
||||
}
|
||||
214
builtin/credential/cert/path_login.go
Normal file
214
builtin/credential/cert/path_login.go
Normal file
@@ -0,0 +1,214 @@
|
||||
package cert
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
|
||||
// TrustedCertificate is a certificate that has been configured as trusted
|
||||
type TrustedCertificate struct {
|
||||
Certificates []*x509.Certificate
|
||||
Policies []string
|
||||
DisplayName string
|
||||
}
|
||||
|
||||
func pathLogin(b *backend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "login",
|
||||
Fields: map[string]*framework.FieldSchema{},
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.WriteOperation: b.pathLogin,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backend) pathLogin(
|
||||
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
// Get the connection state
|
||||
if req.Connection == nil || req.Connection.ConnState == nil {
|
||||
return nil, nil
|
||||
}
|
||||
connState := req.Connection.ConnState
|
||||
|
||||
// Load the trusted certificates
|
||||
roots, trusted := b.loadTrustedCerts(req.Storage)
|
||||
|
||||
// Validate the connection state is trusted
|
||||
trustedChains, err := validateConnState(roots, connState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If no trusted chain was found, client is not authenticated
|
||||
if len(trustedChains) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Match the trusted chain with the policy
|
||||
matched := b.matchPolicy(trustedChains, trusted)
|
||||
if matched == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Generate a response
|
||||
resp := &logical.Response{
|
||||
Auth: &logical.Auth{
|
||||
Policies: matched.Policies,
|
||||
DisplayName: matched.DisplayName,
|
||||
},
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// matchPolicy is used to match the associated policy with the certificate that
|
||||
// was used to establish the client identity.
|
||||
func (b *backend) matchPolicy(chains [][]*x509.Certificate, trusted []*TrustedCertificate) *TrustedCertificate {
|
||||
// There is probably a better way to do this...
|
||||
for _, chain := range chains {
|
||||
for _, trust := range trusted {
|
||||
for _, tCert := range trust.Certificates {
|
||||
for _, cCert := range chain {
|
||||
if tCert.Equal(cCert) {
|
||||
return trust
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadTrustedCerts is used to load all the trusted certificates from the backend
|
||||
func (b *backend) loadTrustedCerts(store logical.Storage) (pool *x509.CertPool, trusted []*TrustedCertificate) {
|
||||
pool = x509.NewCertPool()
|
||||
names, err := b.MapCertId.List(store, "")
|
||||
if err != nil {
|
||||
b.Logger().Printf("[ERR] cert: failed to list trusted certs: %v", err)
|
||||
return
|
||||
}
|
||||
for _, name := range names {
|
||||
data, err := b.MapCertId.Get(store, name)
|
||||
if err != nil {
|
||||
b.Logger().Printf("[ERR] cert: failed to load trusted certs '%s': %v", name, err)
|
||||
continue
|
||||
}
|
||||
certRaw, ok := data["certificate"]
|
||||
if !ok {
|
||||
b.Logger().Printf("[ERR] cert: no certificate for '%s'", name)
|
||||
continue
|
||||
}
|
||||
cert, ok := certRaw.(string)
|
||||
if !ok {
|
||||
b.Logger().Printf("[ERR] cert: certificate for '%s' is not a string", name)
|
||||
continue
|
||||
}
|
||||
parsed := parsePEM([]byte(cert))
|
||||
if len(parsed) == 0 {
|
||||
b.Logger().Printf("[ERR] cert: failed to parse certificate for '%s'", name)
|
||||
continue
|
||||
}
|
||||
for _, p := range parsed {
|
||||
pool.AddCert(p)
|
||||
}
|
||||
|
||||
// Extract the relevant policy
|
||||
var policyString string
|
||||
raw, ok := data["value"]
|
||||
if ok {
|
||||
rawS, ok := raw.(string)
|
||||
if ok {
|
||||
policyString = rawS
|
||||
}
|
||||
}
|
||||
|
||||
// Extract the display name if any
|
||||
var displayName string
|
||||
raw, ok = data["display_name"]
|
||||
if ok {
|
||||
rawS, ok := raw.(string)
|
||||
if ok {
|
||||
displayName = rawS
|
||||
}
|
||||
}
|
||||
|
||||
// Create a TrustedCertificate entry
|
||||
trusted = append(trusted, &TrustedCertificate{
|
||||
Certificates: parsed,
|
||||
Policies: policyStringToList(policyString),
|
||||
DisplayName: displayName,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// policyStringToList turns a string with comma seperated
|
||||
// policies into a sorted, de-duplicated list of policies.
|
||||
func policyStringToList(s string) []string {
|
||||
set := make(map[string]struct{})
|
||||
for _, p := range strings.Split(s, ",") {
|
||||
if p = strings.TrimSpace(p); p != "" {
|
||||
set[p] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
list := make([]string, 0, len(set))
|
||||
for k, _ := range set {
|
||||
list = append(list, k)
|
||||
}
|
||||
sort.Strings(list)
|
||||
return list
|
||||
}
|
||||
|
||||
// parsePEM parses a PEM encoded x509 certificate
|
||||
func parsePEM(raw []byte) (certs []*x509.Certificate) {
|
||||
for len(raw) > 0 {
|
||||
var block *pem.Block
|
||||
block, raw = pem.Decode(raw)
|
||||
if block == nil {
|
||||
break
|
||||
}
|
||||
if block.Type != "CERTIFICATE" || len(block.Headers) != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
certs = append(certs, cert)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// validateConnState is used to validate that the TLS client is authorized
|
||||
// by at trusted certificate. Most of this logic is lifted from the client
|
||||
// verification logic here: http://golang.org/src/crypto/tls/handshake_server.go
|
||||
// The trusted chains are returned.
|
||||
func validateConnState(roots *x509.CertPool, cs *tls.ConnectionState) ([][]*x509.Certificate, error) {
|
||||
opts := x509.VerifyOptions{
|
||||
Roots: roots,
|
||||
Intermediates: x509.NewCertPool(),
|
||||
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
|
||||
}
|
||||
certs := cs.PeerCertificates
|
||||
|
||||
for _, cert := range certs[1:] {
|
||||
opts.Intermediates.AddCert(cert)
|
||||
}
|
||||
|
||||
chains, err := certs[0].Verify(opts)
|
||||
if err != nil {
|
||||
if _, ok := err.(x509.UnknownAuthorityError); ok {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, errors.New("failed to verify client's certificate: " + err.Error())
|
||||
}
|
||||
return chains, nil
|
||||
}
|
||||
Reference in New Issue
Block a user