credential/cert: major refactor
This commit is contained in:
parent
f30c9c1509
commit
ae272b83ce
|
@ -11,42 +11,23 @@ func Factory(map[string]string) (logical.Backend, error) {
|
||||||
|
|
||||||
func Backend() *framework.Backend {
|
func Backend() *framework.Backend {
|
||||||
var b 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{
|
b.Backend = &framework.Backend{
|
||||||
Help: backendHelp,
|
Help: backendHelp,
|
||||||
|
|
||||||
PathsSpecial: &logical.Paths{
|
PathsSpecial: &logical.Paths{
|
||||||
|
Root: []string{
|
||||||
|
"certs/*",
|
||||||
|
},
|
||||||
|
|
||||||
Unauthenticated: []string{
|
Unauthenticated: []string{
|
||||||
"login",
|
"login",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
Paths: framework.PathAppend([]*framework.Path{
|
Paths: append([]*framework.Path{
|
||||||
pathLogin(&b),
|
pathLogin(&b),
|
||||||
},
|
pathCerts(&b),
|
||||||
b.MapCertId.Paths(),
|
}),
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.Backend
|
return b.Backend
|
||||||
|
@ -54,56 +35,16 @@ func Backend() *framework.Backend {
|
||||||
|
|
||||||
type backend struct {
|
type backend struct {
|
||||||
*framework.Backend
|
*framework.Backend
|
||||||
|
MapCertId *framework.PathMap
|
||||||
MapCertId *framework.PolicyMap
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const backendHelp = `
|
const backendHelp = `
|
||||||
The App ID credential provider is used to perform authentication from
|
The "cert" credential provider allows authentication using
|
||||||
within applications or machine by pairing together two hard-to-guess
|
TLS client certificates. A client connects to Vault and uses
|
||||||
unique pieces of information: a unique app ID, and a unique user ID.
|
the "login" endpoint to generate a client token.
|
||||||
|
|
||||||
The goal of this credential provider is to allow elastic users
|
Trusted certificates are configured using the "certs/" endpoint
|
||||||
(dynamic machines, containers, etc.) to authenticate with Vault without
|
by a user with root access. A certificate authority can be trusted,
|
||||||
having to store passwords outside of Vault. It is a single method of
|
which permits all keys signed by it. Alternatively, self-signed
|
||||||
solving the chicken-and-egg problem of setting up Vault access on a machine.
|
certificates can be trusted avoiding the need for a CA.
|
||||||
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.
|
|
||||||
`
|
`
|
||||||
|
|
|
@ -85,10 +85,10 @@ func testAccStepCert(
|
||||||
t *testing.T, name string, cert []byte, policies string) logicaltest.TestStep {
|
t *testing.T, name string, cert []byte, policies string) logicaltest.TestStep {
|
||||||
return logicaltest.TestStep{
|
return logicaltest.TestStep{
|
||||||
Operation: logical.WriteOperation,
|
Operation: logical.WriteOperation,
|
||||||
Path: "map/ca/" + name,
|
Path: "certs/" + name,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"certificate": string(cert),
|
"certificate": string(cert),
|
||||||
"value": policies,
|
"policies": policies,
|
||||||
"display_name": name,
|
"display_name": name,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
134
builtin/credential/cert/path_certs.go
Normal file
134
builtin/credential/cert/path_certs.go
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
package cert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/logical"
|
||||||
|
"github.com/hashicorp/vault/logical/framework"
|
||||||
|
)
|
||||||
|
|
||||||
|
func pathCerts(b *backend) *framework.Path {
|
||||||
|
return &framework.Path{
|
||||||
|
Pattern: `certs/(?P<name>\w+)`,
|
||||||
|
Fields: map[string]*framework.FieldSchema{
|
||||||
|
"name": &framework.FieldSchema{
|
||||||
|
Type: framework.TypeString,
|
||||||
|
Description: "The name of the certificate",
|
||||||
|
},
|
||||||
|
|
||||||
|
"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",
|
||||||
|
},
|
||||||
|
|
||||||
|
"policies": &framework.FieldSchema{
|
||||||
|
Type: framework.TypeString,
|
||||||
|
Description: "Comma-seperated list of policies.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||||
|
logical.DeleteOperation: b.pathCertDelete,
|
||||||
|
logical.ReadOperation: b.pathCertRead,
|
||||||
|
logical.WriteOperation: b.pathCertWrite,
|
||||||
|
},
|
||||||
|
|
||||||
|
HelpSynopsis: pathCertHelpSyn,
|
||||||
|
HelpDescription: pathCertHelpDesc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *backend) Cert(s logical.Storage, n string) (*CertEntry, error) {
|
||||||
|
entry, err := s.Get("cert/" + strings.ToLower(n))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if entry == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var result CertEntry
|
||||||
|
if err := entry.DecodeJSON(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *backend) pathCertDelete(
|
||||||
|
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||||
|
err := req.Storage.Delete("cert/" + strings.ToLower(d.Get("name").(string)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *backend) pathCertRead(
|
||||||
|
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||||
|
cert, err := b.Cert(req.Storage, strings.ToLower(d.Get("name").(string)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if cert == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &logical.Response{
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"certificate": cert.Certificate,
|
||||||
|
"display_name": cert.DisplayName,
|
||||||
|
"policies": strings.Join(cert.Policies, ","),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *backend) pathCertWrite(
|
||||||
|
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||||
|
name := strings.ToLower(d.Get("name").(string))
|
||||||
|
certificate := d.Get("certificate").(string)
|
||||||
|
displayName := d.Get("display_name").(string)
|
||||||
|
policies := strings.Split(d.Get("policies").(string), ",")
|
||||||
|
for i, p := range policies {
|
||||||
|
policies[i] = strings.TrimSpace(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store it
|
||||||
|
entry, err := logical.StorageEntryJSON("cert/"+name, &CertEntry{
|
||||||
|
Name: name,
|
||||||
|
Certificate: certificate,
|
||||||
|
DisplayName: displayName,
|
||||||
|
Policies: policies,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := req.Storage.Put(entry); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type CertEntry struct {
|
||||||
|
Name string
|
||||||
|
Certificate string
|
||||||
|
DisplayName string
|
||||||
|
Policies []string
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathCertHelpSyn = `
|
||||||
|
Manage trusted certificates used for authentication.
|
||||||
|
`
|
||||||
|
|
||||||
|
const pathCertHelpDesc = `
|
||||||
|
This endpoint allows you to create, read, update, and delete trusted certificates
|
||||||
|
that are allowed to authenticate.
|
||||||
|
|
||||||
|
Deleting a certificate will not revoke auth for prior authenticated connections.
|
||||||
|
To do this, do a revoke on "login". If you don't need to revoke login immediately,
|
||||||
|
then the next renew will cause the lease to expire.
|
||||||
|
`
|
|
@ -5,18 +5,16 @@ import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/vault/logical"
|
"github.com/hashicorp/vault/logical"
|
||||||
"github.com/hashicorp/vault/logical/framework"
|
"github.com/hashicorp/vault/logical/framework"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TrustedCertificate is a certificate that has been configured as trusted
|
// ParsedCert is a certificate that has been configured as trusted
|
||||||
type TrustedCertificate struct {
|
type ParsedCert struct {
|
||||||
|
Entry *CertEntry
|
||||||
Certificates []*x509.Certificate
|
Certificates []*x509.Certificate
|
||||||
Policies []string
|
|
||||||
DisplayName string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func pathLogin(b *backend) *framework.Path {
|
func pathLogin(b *backend) *framework.Path {
|
||||||
|
@ -60,8 +58,8 @@ func (b *backend) pathLogin(
|
||||||
// Generate a response
|
// Generate a response
|
||||||
resp := &logical.Response{
|
resp := &logical.Response{
|
||||||
Auth: &logical.Auth{
|
Auth: &logical.Auth{
|
||||||
Policies: matched.Policies,
|
Policies: matched.Entry.Policies,
|
||||||
DisplayName: matched.DisplayName,
|
DisplayName: matched.Entry.DisplayName,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return resp, nil
|
return resp, nil
|
||||||
|
@ -69,7 +67,7 @@ func (b *backend) pathLogin(
|
||||||
|
|
||||||
// matchPolicy is used to match the associated policy with the certificate that
|
// matchPolicy is used to match the associated policy with the certificate that
|
||||||
// was used to establish the client identity.
|
// was used to establish the client identity.
|
||||||
func (b *backend) matchPolicy(chains [][]*x509.Certificate, trusted []*TrustedCertificate) *TrustedCertificate {
|
func (b *backend) matchPolicy(chains [][]*x509.Certificate, trusted []*ParsedCert) *ParsedCert {
|
||||||
// There is probably a better way to do this...
|
// There is probably a better way to do this...
|
||||||
for _, chain := range chains {
|
for _, chain := range chains {
|
||||||
for _, trust := range trusted {
|
for _, trust := range trusted {
|
||||||
|
@ -86,30 +84,20 @@ func (b *backend) matchPolicy(chains [][]*x509.Certificate, trusted []*TrustedCe
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadTrustedCerts is used to load all the trusted certificates from the backend
|
// loadTrustedCerts is used to load all the trusted certificates from the backend
|
||||||
func (b *backend) loadTrustedCerts(store logical.Storage) (pool *x509.CertPool, trusted []*TrustedCertificate) {
|
func (b *backend) loadTrustedCerts(store logical.Storage) (pool *x509.CertPool, trusted []*ParsedCert) {
|
||||||
pool = x509.NewCertPool()
|
pool = x509.NewCertPool()
|
||||||
names, err := b.MapCertId.List(store, "")
|
names, err := store.List("cert/")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Logger().Printf("[ERR] cert: failed to list trusted certs: %v", err)
|
b.Logger().Printf("[ERR] cert: failed to list trusted certs: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
data, err := b.MapCertId.Get(store, name)
|
entry, err := b.Cert(store, strings.TrimPrefix(name, "cert/"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Logger().Printf("[ERR] cert: failed to load trusted certs '%s': %v", name, err)
|
b.Logger().Printf("[ERR] cert: failed to load trusted certs '%s': %v", name, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
certRaw, ok := data["certificate"]
|
parsed := parsePEM([]byte(entry.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 {
|
if len(parsed) == 0 {
|
||||||
b.Logger().Printf("[ERR] cert: failed to parse certificate for '%s'", name)
|
b.Logger().Printf("[ERR] cert: failed to parse certificate for '%s'", name)
|
||||||
continue
|
continue
|
||||||
|
@ -118,54 +106,15 @@ func (b *backend) loadTrustedCerts(store logical.Storage) (pool *x509.CertPool,
|
||||||
pool.AddCert(p)
|
pool.AddCert(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract the relevant policy
|
// Create a ParsedCert entry
|
||||||
var policyString string
|
trusted = append(trusted, &ParsedCert{
|
||||||
raw, ok := data["value"]
|
Entry: entry,
|
||||||
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,
|
Certificates: parsed,
|
||||||
Policies: policyStringToList(policyString),
|
|
||||||
DisplayName: displayName,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return
|
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
|
// parsePEM parses a PEM encoded x509 certificate
|
||||||
func parsePEM(raw []byte) (certs []*x509.Certificate) {
|
func parsePEM(raw []byte) (certs []*x509.Certificate) {
|
||||||
for len(raw) > 0 {
|
for len(raw) > 0 {
|
||||||
|
|
Loading…
Reference in a new issue