diff --git a/CHANGELOG.md b/CHANGELOG.md index 16b0fe02f..c5af2f195 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ FEATURES: IMPROVEMENTS: + * auth/cert: Support for constraints on subject common name, DNS names and + Email addresses in the certificate [GH-2595] * auth/ldap: Use the binding credentials to search group membership rather than the user credentials [GH-2534] * cli/revoke: Add `-self` option to allow revoking the currently active token diff --git a/builtin/credential/cert/backend_test.go b/builtin/credential/cert/backend_test.go index 766c095f8..f96c9cb86 100644 --- a/builtin/credential/cert/backend_test.go +++ b/builtin/credential/cert/backend_test.go @@ -348,9 +348,9 @@ func TestBackend_CertWrites(t *testing.T) { tc := logicaltest.TestCase{ Backend: testFactory(t), Steps: []logicaltest.TestStep{ - testAccStepCert(t, "aaa", ca1, "foo", false), - testAccStepCert(t, "bbb", ca2, "foo", false), - testAccStepCert(t, "ccc", ca3, "foo", true), + testAccStepCert(t, "aaa", ca1, "foo", "", false), + testAccStepCert(t, "bbb", ca2, "foo", "", false), + testAccStepCert(t, "ccc", ca3, "foo", "", true), }, } tc.Steps = append(tc.Steps, testAccStepListCerts(t, []string{"aaa", "bbb"})...) @@ -368,13 +368,17 @@ func TestBackend_basic_CA(t *testing.T) { logicaltest.Test(t, logicaltest.TestCase{ Backend: testFactory(t), Steps: []logicaltest.TestStep{ - testAccStepCert(t, "web", ca, "foo", false), + testAccStepCert(t, "web", ca, "foo", "", false), testAccStepLogin(t, connState), testAccStepCertLease(t, "web", ca, "foo"), testAccStepCertTTL(t, "web", ca, "foo"), testAccStepLogin(t, connState), testAccStepCertNoLease(t, "web", ca, "foo"), testAccStepLoginDefaultLease(t, connState), + testAccStepCert(t, "web", ca, "foo", "*.example.com", false), + testAccStepLogin(t, connState), + testAccStepCert(t, "web", ca, "foo", "*.invalid.com", false), + testAccStepLoginInvalid(t, connState), }, }) } @@ -405,8 +409,29 @@ func TestBackend_Basic_CRLs(t *testing.T) { }) } -// Test a self-signed client that is trusted +// Test a self-signed client (root CA) that is trusted func TestBackend_basic_singleCert(t *testing.T) { + connState := testConnState(t, "test-fixtures/root/rootcacert.pem", + "test-fixtures/root/rootcakey.pem", "test-fixtures/root/rootcacert.pem") + ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem") + if err != nil { + t.Fatalf("err: %v", err) + } + logicaltest.Test(t, logicaltest.TestCase{ + Backend: testFactory(t), + Steps: []logicaltest.TestStep{ + testAccStepCert(t, "web", ca, "foo", "", false), + testAccStepLogin(t, connState), + testAccStepCert(t, "web", ca, "foo", "example.com", false), + testAccStepLogin(t, connState), + testAccStepCert(t, "web", ca, "foo", "invalid", false), + testAccStepLoginInvalid(t, connState), + }, + }) +} + +// Test against a collection of matching and non-matching rules +func TestBackend_mixed_constraints(t *testing.T) { connState := testConnState(t, "test-fixtures/keys/cert.pem", "test-fixtures/keys/key.pem", "test-fixtures/root/rootcacert.pem") ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem") @@ -416,13 +441,18 @@ func TestBackend_basic_singleCert(t *testing.T) { logicaltest.Test(t, logicaltest.TestCase{ Backend: testFactory(t), Steps: []logicaltest.TestStep{ - testAccStepCert(t, "web", ca, "foo", false), + testAccStepCert(t, "1unconstrained", ca, "foo", "", false), + testAccStepCert(t, "2matching", ca, "foo", "*.example.com,whatever", false), + testAccStepCert(t, "3invalid", ca, "foo", "invalid", false), testAccStepLogin(t, connState), + // Assumes CertEntries are processed in alphabetical order (due to store.List), so we only match 2matching if 1unconstrained doesn't match + testAccStepLoginWithName(t, connState, "2matching"), + testAccStepLoginWithNameInvalid(t, connState, "3invalid"), }, }) } -// Test an untrusted self-signed client +// Test an untrusted client func TestBackend_untrusted(t *testing.T) { connState := testConnState(t, "test-fixtures/keys/cert.pem", "test-fixtures/keys/key.pem", "test-fixtures/root/rootcacert.pem") @@ -476,6 +506,10 @@ func testAccStepDeleteCRL(t *testing.T, connState tls.ConnectionState) logicalte } func testAccStepLogin(t *testing.T, connState tls.ConnectionState) logicaltest.TestStep { + return testAccStepLoginWithName(t, connState, "") +} + +func testAccStepLoginWithName(t *testing.T, connState tls.ConnectionState, certName string) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.UpdateOperation, Path: "login", @@ -486,9 +520,16 @@ func testAccStepLogin(t *testing.T, connState tls.ConnectionState) logicaltest.T t.Fatalf("bad lease length: %#v", resp.Auth) } + if certName != "" && resp.Auth.DisplayName != ("mnt-"+certName) { + t.Fatalf("matched the wrong cert: %#v", resp.Auth.DisplayName) + } + fn := logicaltest.TestCheckAuth([]string{"default", "foo"}) return fn(resp) }, + Data: map[string]interface{}{ + "name": certName, + }, } } @@ -510,6 +551,10 @@ func testAccStepLoginDefaultLease(t *testing.T, connState tls.ConnectionState) l } func testAccStepLoginInvalid(t *testing.T, connState tls.ConnectionState) logicaltest.TestStep { + return testAccStepLoginWithNameInvalid(t, connState, "") +} + +func testAccStepLoginWithNameInvalid(t *testing.T, connState tls.ConnectionState, certName string) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.UpdateOperation, Path: "login", @@ -521,6 +566,9 @@ func testAccStepLoginInvalid(t *testing.T, connState tls.ConnectionState) logica } return nil }, + Data: map[string]interface{}{ + "name": certName, + }, ErrorOk: true, } } @@ -572,16 +620,17 @@ func testAccStepListCerts( } func testAccStepCert( - t *testing.T, name string, cert []byte, policies string, expectError bool) logicaltest.TestStep { + t *testing.T, name string, cert []byte, policies string, allowedNames string, expectError bool) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.UpdateOperation, Path: "certs/" + name, ErrorOk: expectError, Data: map[string]interface{}{ - "certificate": string(cert), - "policies": policies, - "display_name": name, - "lease": 1000, + "certificate": string(cert), + "policies": policies, + "display_name": name, + "allowed_names": allowedNames, + "lease": 1000, }, Check: func(resp *logical.Response) error { if resp == nil && expectError { @@ -730,10 +779,17 @@ func Test_Renew(t *testing.T) { t.Fatal(err) } - resp, err = b.pathLogin(req, nil) + empty_login_fd := &framework.FieldData{ + Raw: map[string]interface{}{}, + Schema: pathLogin(b).Fields, + } + resp, err = b.pathLogin(req, empty_login_fd) if err != nil { t.Fatal(err) } + if resp.IsError() { + t.Fatalf("got error: %#v", *resp) + } req.Auth.InternalData = resp.Auth.InternalData req.Auth.Metadata = resp.Auth.Metadata req.Auth.LeaseOptions = resp.Auth.LeaseOptions @@ -741,7 +797,7 @@ func Test_Renew(t *testing.T) { req.Auth.IssueTime = time.Now() // Normal renewal - resp, err = b.pathLoginRenew(req, nil) + resp, err = b.pathLoginRenew(req, empty_login_fd) if err != nil { t.Fatal(err) } @@ -759,7 +815,7 @@ func Test_Renew(t *testing.T) { t.Fatal(err) } - resp, err = b.pathLoginRenew(req, nil) + resp, err = b.pathLoginRenew(req, empty_login_fd) if err == nil { t.Fatal("expected error") } @@ -771,7 +827,7 @@ func Test_Renew(t *testing.T) { t.Fatal(err) } - resp, err = b.pathLoginRenew(req, nil) + resp, err = b.pathLoginRenew(req, empty_login_fd) if err != nil { t.Fatal(err) } @@ -788,7 +844,7 @@ func Test_Renew(t *testing.T) { t.Fatal(err) } - resp, err = b.pathLoginRenew(req, nil) + resp, err = b.pathLoginRenew(req, empty_login_fd) if err != nil { t.Fatal(err) } diff --git a/builtin/credential/cert/cli.go b/builtin/credential/cert/cli.go index 4afc1eadc..66809c2e3 100644 --- a/builtin/credential/cert/cli.go +++ b/builtin/credential/cert/cli.go @@ -13,6 +13,7 @@ type CLIHandler struct{} func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { var data struct { Mount string `mapstructure:"mount"` + Name string `mapstructure:"name"` } if err := mapstructure.WeakDecode(m, &data); err != nil { return "", err @@ -22,8 +23,11 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) { data.Mount = "cert" } + options := map[string]interface{}{ + "name": data.Name, + } path := fmt.Sprintf("auth/%s/login", data.Mount) - secret, err := c.Logical().Write(path, nil) + secret, err := c.Logical().Write(path, options) if err != nil { return "", err } @@ -38,10 +42,13 @@ func (h *CLIHandler) Help() string { help := ` The "cert" credential provider allows you to authenticate with a client certificate. No other authentication materials are needed. +Optionally, you may specify the specific certificate role to +authenticate against with the "name" parameter. Example: vault auth -method=cert \ -client-cert=/path/to/cert.pem \ -client-key=/path/to/key.pem + name=cert1 ` diff --git a/builtin/credential/cert/path_certs.go b/builtin/credential/cert/path_certs.go index d842bb841..2c002f6e3 100644 --- a/builtin/credential/cert/path_certs.go +++ b/builtin/credential/cert/path_certs.go @@ -39,6 +39,12 @@ func pathCerts(b *backend) *framework.Path { Must be x509 PEM encoded.`, }, + "allowed_names": &framework.FieldSchema{ + Type: framework.TypeCommaStringSlice, + Description: `A comma-separated list of names. +At least one must exist in either the Common Name or SANs. Supports globbing.`, + }, + "display_name": &framework.FieldSchema{ Type: framework.TypeString, Description: `The display name to use for clients using this @@ -139,6 +145,7 @@ func (b *backend) pathCertWrite( certificate := d.Get("certificate").(string) displayName := d.Get("display_name").(string) policies := policyutil.ParsePolicies(d.Get("policies").(string)) + allowedNames := d.Get("allowed_names").([]string) // Default the display name to the certificate name if not given if displayName == "" { @@ -165,10 +172,11 @@ func (b *backend) pathCertWrite( } certEntry := &CertEntry{ - Name: name, - Certificate: certificate, - DisplayName: displayName, - Policies: policies, + Name: name, + Certificate: certificate, + DisplayName: displayName, + Policies: policies, + AllowedNames: allowedNames, } // Parse the lease duration or default to backend/system default @@ -196,11 +204,12 @@ func (b *backend) pathCertWrite( } type CertEntry struct { - Name string - Certificate string - DisplayName string - Policies []string - TTL time.Duration + Name string + Certificate string + DisplayName string + Policies []string + TTL time.Duration + AllowedNames []string } const pathCertHelpSyn = ` diff --git a/builtin/credential/cert/path_login.go b/builtin/credential/cert/path_login.go index d9fcbe8fd..164bbe7de 100644 --- a/builtin/credential/cert/path_login.go +++ b/builtin/credential/cert/path_login.go @@ -14,6 +14,8 @@ import ( "github.com/hashicorp/vault/helper/policyutil" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" + + "github.com/ryanuber/go-glob" ) // ParsedCert is a certificate that has been configured as trusted @@ -25,7 +27,12 @@ type ParsedCert struct { func pathLogin(b *backend) *framework.Path { return &framework.Path{ Pattern: "login", - Fields: map[string]*framework.FieldSchema{}, + Fields: map[string]*framework.FieldSchema{ + "name": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "The name of the certificate role to authenticate against.", + }, + }, Callbacks: map[logical.Operation]framework.OperationFunc{ logical.UpdateOperation: b.pathLogin, }, @@ -36,7 +43,7 @@ func (b *backend) pathLogin( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { var matched *ParsedCert - if verifyResp, resp, err := b.verifyCredentials(req); err != nil { + if verifyResp, resp, err := b.verifyCredentials(req, data); err != nil { return nil, err } else if resp != nil { return resp, nil @@ -93,7 +100,7 @@ func (b *backend) pathLoginRenew( if !config.DisableBinding { var matched *ParsedCert - if verifyResp, resp, err := b.verifyCredentials(req); err != nil { + if verifyResp, resp, err := b.verifyCredentials(req, d); err != nil { return nil, err } else if resp != nil { return resp, nil @@ -136,7 +143,7 @@ func (b *backend) pathLoginRenew( return framework.LeaseExtend(cert.TTL, 0, b.System())(req, d) } -func (b *backend) verifyCredentials(req *logical.Request) (*ParsedCert, *logical.Response, error) { +func (b *backend) verifyCredentials(req *logical.Request, d *framework.FieldData) (*ParsedCert, *logical.Response, error) { // Get the connection state if req.Connection == nil || req.Connection.ConnState == nil { return nil, logical.ErrorResponse("tls connection required"), nil @@ -146,20 +153,29 @@ func (b *backend) verifyCredentials(req *logical.Request) (*ParsedCert, *logical if connState.PeerCertificates == nil || len(connState.PeerCertificates) == 0 { return nil, logical.ErrorResponse("client certificate must be supplied"), nil } + clientCert := connState.PeerCertificates[0] + + // Allow constraining the login request to a single CertEntry + certName := d.Get("name").(string) // Load the trusted certificates - roots, trusted, trustedNonCAs := b.loadTrustedCerts(req.Storage) + roots, trusted, trustedNonCAs := b.loadTrustedCerts(req.Storage, certName) // If trustedNonCAs is not empty it means that client had registered a non-CA cert // with the backend. if len(trustedNonCAs) != 0 { - policy := b.matchNonCAPolicy(connState.PeerCertificates[0], trustedNonCAs) - if policy != nil && !b.checkForChainInCRLs(policy.Certificates) { - return policy, nil, nil + for _, trustedNonCA := range trustedNonCAs { + tCert := trustedNonCA.Certificates[0] + // Check for client cert being explicitly listed in the config (and matching other constraints) + if tCert.SerialNumber.Cmp(clientCert.SerialNumber) == 0 && + bytes.Equal(tCert.AuthorityKeyId, clientCert.AuthorityKeyId) && + b.matchesConstraints(clientCert, trustedNonCA.Certificates, trustedNonCA) { + return trustedNonCA, nil, nil + } } } - // Validate the connection state is trusted + // Get the list of full chains matching the connection trustedChains, err := validateConnState(roots, connState) if err != nil { return nil, nil, err @@ -170,49 +186,58 @@ func (b *backend) verifyCredentials(req *logical.Request) (*ParsedCert, *logical return nil, logical.ErrorResponse("invalid certificate or no client certificate supplied"), nil } - validChain := b.checkForValidChain(trustedChains) - if !validChain { - return nil, logical.ErrorResponse( - "no chain containing non-revoked certificates could be found for this login certificate", - ), nil - } - - // Match the trusted chain with the policy - return b.matchPolicy(trustedChains, trusted), nil, nil -} - -// matchNonCAPolicy is used to match the client cert with the registered non-CA -// policies to establish client identity. -func (b *backend) matchNonCAPolicy(clientCert *x509.Certificate, trustedNonCAs []*ParsedCert) *ParsedCert { - for _, trustedNonCA := range trustedNonCAs { - tCert := trustedNonCA.Certificates[0] - if tCert.SerialNumber.Cmp(clientCert.SerialNumber) == 0 && bytes.Equal(tCert.AuthorityKeyId, clientCert.AuthorityKeyId) { - return trustedNonCA - } - } - return 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 []*ParsedCert) *ParsedCert { - // 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 + // Search for a ParsedCert that intersects with the validated chains and any additional constraints + matches := make([]*ParsedCert, 0) + for _, trust := range trusted { // For each ParsedCert in the config + for _, tCert := range trust.Certificates { // For each certificate in the entry + for _, chain := range trustedChains { // For each root chain that we matched + for _, cCert := range chain { // For each cert in the matched chain + if tCert.Equal(cCert) && // ParsedCert intersects with matched chain + b.matchesConstraints(clientCert, chain, trust) { // validate client cert + matched chain against the config + // Add the match to the list + matches = append(matches, trust) } } } } } - return nil + + // Fail on no matches + if len(matches) == 0 { + return nil, logical.ErrorResponse("no chain matching all constraints could be found for this login certificate"), nil + } + + // Return the first matching entry (for backwards compatibility, we continue to just pick one if multiple match) + return matches[0], nil, nil +} + +func (b *backend) matchesConstraints(clientCert *x509.Certificate, trustedChain []*x509.Certificate, config *ParsedCert) bool { + // Default behavior (no names) is to allow all names + nameMatched := len(config.Entry.AllowedNames) == 0 + // At least one pattern must match at least one name if any patterns are specified + for _, allowedName := range config.Entry.AllowedNames { + if glob.Glob(allowedName, clientCert.Subject.CommonName) { + nameMatched = true + } + + for _, name := range clientCert.DNSNames { + if glob.Glob(allowedName, name) { + nameMatched = true + } + } + + for _, name := range clientCert.EmailAddresses { + if glob.Glob(allowedName, name) { + nameMatched = true + } + } + } + + return !b.checkForChainInCRLs(trustedChain) && nameMatched } // loadTrustedCerts is used to load all the trusted certificates from the backend -func (b *backend) loadTrustedCerts(store logical.Storage) (pool *x509.CertPool, trusted []*ParsedCert, trustedNonCAs []*ParsedCert) { +func (b *backend) loadTrustedCerts(store logical.Storage, certName string) (pool *x509.CertPool, trusted []*ParsedCert, trustedNonCAs []*ParsedCert) { pool = x509.NewCertPool() trusted = make([]*ParsedCert, 0) trustedNonCAs = make([]*ParsedCert, 0) @@ -222,6 +247,10 @@ func (b *backend) loadTrustedCerts(store logical.Storage) (pool *x509.CertPool, return } for _, name := range names { + // If we are trying to select a single CertEntry and this isn't it + if certName != "" && name != certName { + continue + } entry, err := b.Cert(store, strings.TrimPrefix(name, "cert/")) if err != nil { b.Logger().Error("cert: failed to load trusted cert", "name", name, "error", err) diff --git a/builtin/logical/pki/cert_util.go b/builtin/logical/pki/cert_util.go index b688f7caa..e84e238c1 100644 --- a/builtin/logical/pki/cert_util.go +++ b/builtin/logical/pki/cert_util.go @@ -466,7 +466,7 @@ func signCert(b *backend, } csr, err := x509.ParseCertificateRequest(pemBlock.Bytes) if err != nil { - return nil, errutil.UserError{Err: "certificate request could not be parsed"} + return nil, errutil.UserError{Err: fmt.Sprintf("certificate request could not be parsed: %v", err)} } switch role.KeyType { @@ -970,7 +970,7 @@ func createCSR(creationInfo *creationBundle) (*certutil.ParsedCSRBundle, error) result.CSRBytes = csr result.CSR, err = x509.ParseCertificateRequest(csr) if err != nil { - return nil, errutil.InternalError{Err: fmt.Sprintf("unable to parse created certificate: %s", err)} + return nil, errutil.InternalError{Err: fmt.Sprintf("unable to parse created certificate: %v", err)} } return result, nil diff --git a/helper/certutil/types.go b/helper/certutil/types.go index aa8d115f0..35b7317a5 100644 --- a/helper/certutil/types.go +++ b/helper/certutil/types.go @@ -444,7 +444,7 @@ func (c *CSRBundle) ToParsedCSRBundle() (*ParsedCSRBundle, error) { result.CSRBytes = pemBlock.Bytes result.CSR, err = x509.ParseCertificateRequest(result.CSRBytes) if err != nil { - return nil, errutil.UserError{"Error encountered parsing certificate bytes from raw bundle"} + return nil, errutil.UserError{fmt.Sprintf("Error encountered parsing certificate bytes from raw bundle: %v", err)} } } diff --git a/shamir/shamir.go b/shamir/shamir.go index 96d48361a..d6f5137e5 100644 --- a/shamir/shamir.go +++ b/shamir/shamir.go @@ -4,6 +4,8 @@ import ( "crypto/rand" "crypto/subtle" "fmt" + mathrand "math/rand" + "time" ) const ( @@ -166,13 +168,17 @@ func Split(secret []byte, parts, threshold int) ([][]byte, error) { return nil, fmt.Errorf("cannot split an empty secret") } + // Generate random list of x coordinates + mathrand.Seed(time.Now().UnixNano()) + xCoordinates := mathrand.Perm(255) + // Allocate the output array, initialize the final byte // of the output with the offset. The representation of each // output is {y1, y2, .., yN, x}. out := make([][]byte, parts) for idx := range out { out[idx] = make([]byte, len(secret)+1) - out[idx][len(secret)] = uint8(idx) + 1 + out[idx][len(secret)] = uint8(xCoordinates[idx]) + 1 } // Construct a random polynomial for each byte of the secret. @@ -189,7 +195,7 @@ func Split(secret []byte, parts, threshold int) ([][]byte, error) { // We cheat by encoding the x value once as the final index, // so that it only needs to be stored once. for i := 0; i < parts; i++ { - x := uint8(i) + 1 + x := uint8(xCoordinates[i]) + 1 y := p.evaluate(x) out[i][idx] = y } diff --git a/vendor/github.com/ryanuber/go-glob/LICENSE b/vendor/github.com/ryanuber/go-glob/LICENSE new file mode 100644 index 000000000..bdfbd9514 --- /dev/null +++ b/vendor/github.com/ryanuber/go-glob/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Ryan Uber + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/ryanuber/go-glob/README.md b/vendor/github.com/ryanuber/go-glob/README.md new file mode 100644 index 000000000..48f7fcb05 --- /dev/null +++ b/vendor/github.com/ryanuber/go-glob/README.md @@ -0,0 +1,29 @@ +# String globbing in golang [![Build Status](https://travis-ci.org/ryanuber/go-glob.svg)](https://travis-ci.org/ryanuber/go-glob) + +`go-glob` is a single-function library implementing basic string glob support. + +Globs are an extremely user-friendly way of supporting string matching without +requiring knowledge of regular expressions or Go's particular regex engine. Most +people understand that if you put a `*` character somewhere in a string, it is +treated as a wildcard. Surprisingly, this functionality isn't found in Go's +standard library, except for `path.Match`, which is intended to be used while +comparing paths (not arbitrary strings), and contains specialized logic for this +use case. A better solution might be a POSIX basic (non-ERE) regular expression +engine for Go, which doesn't exist currently. + +Example +======= + +``` +package main + +import "github.com/ryanuber/go-glob" + +func main() { + glob.Glob("*World!", "Hello, World!") // true + glob.Glob("Hello,*", "Hello, World!") // true + glob.Glob("*ello,*", "Hello, World!") // true + glob.Glob("World!", "Hello, World!") // false + glob.Glob("/home/*", "/home/ryanuber/.bashrc") // true +} +``` diff --git a/vendor/github.com/ryanuber/go-glob/glob.go b/vendor/github.com/ryanuber/go-glob/glob.go new file mode 100644 index 000000000..e67db3be1 --- /dev/null +++ b/vendor/github.com/ryanuber/go-glob/glob.go @@ -0,0 +1,56 @@ +package glob + +import "strings" + +// The character which is treated like a glob +const GLOB = "*" + +// Glob will test a string pattern, potentially containing globs, against a +// subject string. The result is a simple true/false, determining whether or +// not the glob pattern matched the subject text. +func Glob(pattern, subj string) bool { + // Empty pattern can only match empty subject + if pattern == "" { + return subj == pattern + } + + // If the pattern _is_ a glob, it matches everything + if pattern == GLOB { + return true + } + + parts := strings.Split(pattern, GLOB) + + if len(parts) == 1 { + // No globs in pattern, so test for equality + return subj == pattern + } + + leadingGlob := strings.HasPrefix(pattern, GLOB) + trailingGlob := strings.HasSuffix(pattern, GLOB) + end := len(parts) - 1 + + // Go over the leading parts and ensure they match. + for i := 0; i < end; i++ { + idx := strings.Index(subj, parts[i]) + + switch i { + case 0: + // Check the first section. Requires special handling. + if !leadingGlob && idx != 0 { + return false + } + default: + // Check that the middle parts match. + if idx < 0 { + return false + } + } + + // Trim evaluated text from subj as we loop over the pattern. + subj = subj[idx+len(parts[i]):] + } + + // Reached the last section. Requires special handling. + return trailingGlob || strings.HasSuffix(subj, parts[end]) +} diff --git a/vendor/github.com/sstarcher/go-okta/api.go b/vendor/github.com/sstarcher/go-okta/api.go index 546bfb450..1236f0744 100644 --- a/vendor/github.com/sstarcher/go-okta/api.go +++ b/vendor/github.com/sstarcher/go-okta/api.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "io/ioutil" - "log" "net/http" ) @@ -84,7 +83,7 @@ func (c *Client) call(endpoint, method string, request, response interface{}) er var url = "https://" + c.org + "." + c.Url + "/api/v1/" + endpoint req, err := http.NewRequest(method, url, bytes.NewBuffer(data)) if err != nil { - log.Fatal(err) + return err } req.Header.Add("Accept", `application/json`) @@ -95,19 +94,19 @@ func (c *Client) call(endpoint, method string, request, response interface{}) er resp, err := c.client.Do(req) if err != nil { - log.Fatal(err) + return err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { - log.Fatal(err) + return err } if resp.StatusCode == http.StatusOK { err := json.Unmarshal(body, &response) if err != nil { - log.Fatal(err) + return err } } else { var errors ErrorResponse diff --git a/vendor/vendor.json b/vendor/vendor.json index 52d711de9..4f5912d19 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -1158,6 +1158,12 @@ "revision": "ddeb643de91b4ee0d9d87172c931a4ea3d81d49a", "revisionTime": "2017-02-08T17:17:27Z" }, + { + "checksumSHA1": "6JP37UqrI0H80Gpk0Y2P+KXgn5M=", + "path": "github.com/ryanuber/go-glob", + "revision": "256dc444b735e061061cf46c809487313d5b0065", + "revisionTime": "2017-01-28T01:21:29Z" + }, { "checksumSHA1": "5SYLEhADhdBVZAGPVHWggQl7H8k=", "path": "github.com/samuel/go-zookeeper/zk", @@ -1171,10 +1177,10 @@ "revisionTime": "2017-04-08T21:24:09Z" }, { - "checksumSHA1": "reJ+wO9qzH/7r2vXQE5MiTvg8+w=", + "checksumSHA1": "7b7psq20O8IOCr885W2Ld6a3KTc=", "path": "github.com/sstarcher/go-okta", - "revision": "388b6aef4eed400621bd3e3a98d831ef1368582d", - "revisionTime": "2016-10-03T17:19:47Z" + "revision": "64b3cb9e3a7b6d0c4e4432576c873e492d152666", + "revisionTime": "2017-04-28T20:44:25Z" }, { "checksumSHA1": "MxLnUmfrP+r5HfCZM29+WPKebn8=", diff --git a/version/version.go b/version/version.go index b7985ae0f..0f8193335 100644 --- a/version/version.go +++ b/version/version.go @@ -15,6 +15,7 @@ var ( Version = "unknown" VersionPrerelease = "unknown" + VersionMetadata = "" ) // VersionInfo @@ -22,11 +23,13 @@ type VersionInfo struct { Revision string Version string VersionPrerelease string + VersionMetadata string } func GetVersion() *VersionInfo { ver := Version rel := VersionPrerelease + md := VersionMetadata if GitDescribe != "" { ver = GitDescribe } @@ -38,6 +41,7 @@ func GetVersion() *VersionInfo { Revision: GitCommit, Version: ver, VersionPrerelease: rel, + VersionMetadata: md, } } @@ -52,6 +56,10 @@ func (c *VersionInfo) VersionNumber() string { version = fmt.Sprintf("%s-%s", version, c.VersionPrerelease) } + if c.VersionMetadata != "" { + version = fmt.Sprintf("%s+%s", version, c.VersionMetadata) + } + return version } @@ -66,6 +74,11 @@ func (c *VersionInfo) FullVersionNumber(rev bool) string { if c.VersionPrerelease != "" { fmt.Fprintf(&versionString, "-%s", c.VersionPrerelease) } + + if c.VersionMetadata != "" { + fmt.Fprintf(&versionString, "+%s", c.VersionMetadata) + } + if rev && c.Revision != "" { fmt.Fprintf(&versionString, " (%s)", c.Revision) } diff --git a/website/source/api/system/audit.html.md b/website/source/api/system/audit.html.md index 794497c36..060756bce 100644 --- a/website/source/api/system/audit.html.md +++ b/website/source/api/system/audit.html.md @@ -71,6 +71,13 @@ single word name or a more complex, nested path. - `type` `(string: )` – Specifies the type of the audit backend. +Additionally, the following options are allowed in Vault open-source, but +relevant functionality is only supported in Vault Enterprise: + +- `local` `(bool: false)` – Specifies if the audit backend is a local mount + only. Local mounts are not replicated nor (if a secondary) removed by + replication. + ### Sample Payload ```json diff --git a/website/source/api/system/auth.html.md b/website/source/api/system/auth.html.md index e5c4d0cea..eb9a61d79 100644 --- a/website/source/api/system/auth.html.md +++ b/website/source/api/system/auth.html.md @@ -74,6 +74,13 @@ For example, mounting the "foo" auth backend will make it accessible at - `type` `(string: )` – Specifies the name of the authentication backend type, such as "github" or "token". +Additionally, the following options are allowed in Vault open-source, but +relevant functionality is only supported in Vault Enterprise: + +- `local` `(bool: false)` – Specifies if the auth backend is a local mount + only. Local mounts are not replicated nor (if a secondary) removed by + replication. + ### Sample Payload ```json diff --git a/website/source/api/system/mounts.html.md b/website/source/api/system/mounts.html.md index 67713f6fb..8b485f5ee 100644 --- a/website/source/api/system/mounts.html.md +++ b/website/source/api/system/mounts.html.md @@ -84,6 +84,13 @@ This endpoint mounts a new secret backend at the given path. disabling backend caching respectively. If set on a specific mount, this overrides the global defaults. +Additionally, the following options are allowed in Vault open-source, but +relevant functionality is only supported in Vault Enterprise: + +- `local` `(bool: false)` – Specifies if the secret backend is a local mount + only. Local mounts are not replicated nor (if a secondary) removed by + replication. + ### Sample Payload ```json diff --git a/website/source/api/system/wrapping-wrap.html.md b/website/source/api/system/wrapping-wrap.html.md index c10cdbbaf..c3c4065d5 100644 --- a/website/source/api/system/wrapping-wrap.html.md +++ b/website/source/api/system/wrapping-wrap.html.md @@ -41,6 +41,7 @@ token. ``` $ curl \ --header "X-Vault-Token: ..." \ + --header "X-Vault-Wrap-TTL: 60" \ --request POST \ --data @payload.json \ https://vault.rocks/v1/sys/wrapping/wrap diff --git a/website/source/docs/auth/cert.html.md b/website/source/docs/auth/cert.html.md index a9e50c6c0..4f094053e 100644 --- a/website/source/docs/auth/cert.html.md +++ b/website/source/docs/auth/cert.html.md @@ -60,18 +60,25 @@ it is up to the administrator to remove it from the backend. ## Authentication ### Via the CLI +The below requires Vault to present a certificate signed by `ca.pem` and +presents `cert.pem` (using `key.pem`) to authenticate against the `web` cert +role. If a certificate role name is not specified, the auth backend will try to +authenticate against all trusted certificates. + ``` $ vault auth -method=cert \ - -ca-cert=ca.pem -client-cert=cert.pem -client-key=key.pem + -ca-cert=ca.pem -client-cert=cert.pem -client-key=key.pem \ + name=web ``` ### Via the API The endpoint for the login is `/login`. The client simply connects with their TLS certificate and when the login endpoint is hit, the auth backend will determine -if there is a matching trusted certificate to authenticate the client. +if there is a matching trusted certificate to authenticate the client. Optionally, +you may specify a single certificate role to authenticate against. ``` -$ curl --cacert ca.pem --cert cert.pem --key key.pem \ +$ curl --cacert ca.pem --cert cert.pem --key key.pem -d name=web \ $VAULT_ADDR/v1/auth/cert/login -XPOST ``` @@ -175,6 +182,7 @@ of the header should be "X-Vault-Token" and the value should be the token. "certificate": "-----BEGIN CERTIFICATE-----\nMIIEtzCCA5+.......ZRtAfQ6r\nwlW975rYa1ZqEdA=\n-----END CERTIFICATE-----", "display_name": "test", "policies": "", + "allowed_names": "", "ttl": 2764800 }, "warnings": null, @@ -245,6 +253,15 @@ of the header should be "X-Vault-Token" and the value should be the token. required The PEM-format CA certificate. +
  • + allowed_names + optional + Constrain the Common and Alternative Names in the client certificate + with a [globbed pattern](https://github.com/ryanuber/go-glob/blob/master/README.md#example). + Value is a comma-separated list of patterns. + Authentication requires at least one Name matching at least one pattern. + If not set, defaults to allowing all names. +
  • policies optional @@ -382,8 +399,8 @@ of the header should be "X-Vault-Token" and the value should be the token.
    Description
    - Log in and fetch a token. If there is a valid chain to a CA configured in the backend, - a token will be issued. + Log in and fetch a token. If there is a valid chain to a CA configured in + the backend and all role constraints are matched, a token will be issued.
    Method
    @@ -394,7 +411,15 @@ of the header should be "X-Vault-Token" and the value should be the token.
    Parameters
    - None. +
      +
    • + name + optional + Authenticate against only the named certificate role, returning its + policy list if successful. If not set, defaults to trying all + certificate roles and returning any one that matches. +
    • +
    Returns
    diff --git a/website/source/docs/configuration/storage/consul.html.md b/website/source/docs/configuration/storage/consul.html.md index 49c73afcb..821ea5e7c 100644 --- a/website/source/docs/configuration/storage/consul.html.md +++ b/website/source/docs/configuration/storage/consul.html.md @@ -193,6 +193,6 @@ storage "consul" { ``` [consul]: https://www.consul.io/ "Consul by HashiCorp" -[consul-acl]: https://www.consul.io/docs/internals/acl.html "Consul ACLs" -[consul-consistency]: https://www.consul.io/docs/agent/http.html#consistency-modes "Consul Consistency Modes" +[consul-acl]: https://www.consul.io/docs/guides/acl.html "Consul ACLs" +[consul-consistency]: https://www.consul.io/api/index.html#consistency-modes "Consul Consistency Modes" [consul-encryption]: https://www.consul.io/docs/agent/encryption.html "Consul Encryption"