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 {
|
||||
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{
|
||||
Root: []string{
|
||||
"certs/*",
|
||||
},
|
||||
|
||||
Unauthenticated: []string{
|
||||
"login",
|
||||
},
|
||||
},
|
||||
|
||||
Paths: framework.PathAppend([]*framework.Path{
|
||||
Paths: append([]*framework.Path{
|
||||
pathLogin(&b),
|
||||
},
|
||||
b.MapCertId.Paths(),
|
||||
),
|
||||
pathCerts(&b),
|
||||
}),
|
||||
}
|
||||
|
||||
return b.Backend
|
||||
|
@ -54,56 +35,16 @@ func Backend() *framework.Backend {
|
|||
|
||||
type backend struct {
|
||||
*framework.Backend
|
||||
|
||||
MapCertId *framework.PolicyMap
|
||||
MapCertId *framework.PathMap
|
||||
}
|
||||
|
||||
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 "cert" credential provider allows authentication using
|
||||
TLS client certificates. A client connects to Vault and uses
|
||||
the "login" endpoint to generate a client token.
|
||||
|
||||
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.
|
||||
Trusted certificates are configured using the "certs/" endpoint
|
||||
by a user with root access. A certificate authority can be trusted,
|
||||
which permits all keys signed by it. Alternatively, self-signed
|
||||
certificates can be trusted avoiding the need for a CA.
|
||||
`
|
||||
|
|
|
@ -85,10 +85,10 @@ func testAccStepCert(
|
|||
t *testing.T, name string, cert []byte, policies string) logicaltest.TestStep {
|
||||
return logicaltest.TestStep{
|
||||
Operation: logical.WriteOperation,
|
||||
Path: "map/ca/" + name,
|
||||
Path: "certs/" + name,
|
||||
Data: map[string]interface{}{
|
||||
"certificate": string(cert),
|
||||
"value": policies,
|
||||
"policies": policies,
|
||||
"display_name": name,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
"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 {
|
||||
// ParsedCert is a certificate that has been configured as trusted
|
||||
type ParsedCert struct {
|
||||
Entry *CertEntry
|
||||
Certificates []*x509.Certificate
|
||||
Policies []string
|
||||
DisplayName string
|
||||
}
|
||||
|
||||
func pathLogin(b *backend) *framework.Path {
|
||||
|
@ -60,8 +58,8 @@ func (b *backend) pathLogin(
|
|||
// Generate a response
|
||||
resp := &logical.Response{
|
||||
Auth: &logical.Auth{
|
||||
Policies: matched.Policies,
|
||||
DisplayName: matched.DisplayName,
|
||||
Policies: matched.Entry.Policies,
|
||||
DisplayName: matched.Entry.DisplayName,
|
||||
},
|
||||
}
|
||||
return resp, nil
|
||||
|
@ -69,7 +67,7 @@ func (b *backend) pathLogin(
|
|||
|
||||
// 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 {
|
||||
func (b *backend) matchPolicy(chains [][]*x509.Certificate, trusted []*ParsedCert) *ParsedCert {
|
||||
// There is probably a better way to do this...
|
||||
for _, chain := range chains {
|
||||
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
|
||||
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()
|
||||
names, err := b.MapCertId.List(store, "")
|
||||
names, err := store.List("cert/")
|
||||
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)
|
||||
entry, err := b.Cert(store, strings.TrimPrefix(name, "cert/"))
|
||||
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))
|
||||
parsed := parsePEM([]byte(entry.Certificate))
|
||||
if len(parsed) == 0 {
|
||||
b.Logger().Printf("[ERR] cert: failed to parse certificate for '%s'", name)
|
||||
continue
|
||||
|
@ -118,54 +106,15 @@ func (b *backend) loadTrustedCerts(store logical.Storage) (pool *x509.CertPool,
|
|||
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{
|
||||
// Create a ParsedCert entry
|
||||
trusted = append(trusted, &ParsedCert{
|
||||
Entry: entry,
|
||||
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 {
|
||||
|
|
Loading…
Reference in New Issue