From 37297080f20cd78f1a703b9d3db4e9bfd9e81c05 Mon Sep 17 00:00:00 2001 From: Vishal Nayak Date: Thu, 16 Feb 2017 00:51:02 -0500 Subject: [PATCH 01/30] cidrutil: added test data points (#2378) --- builtin/credential/approle/validation.go | 6 +++--- helper/cidrutil/cidr_test.go | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/builtin/credential/approle/validation.go b/builtin/credential/approle/validation.go index 7582b6daa9..b4d3af154f 100644 --- a/builtin/credential/approle/validation.go +++ b/builtin/credential/approle/validation.go @@ -143,7 +143,7 @@ func (b *backend) validateCredentials(req *logical.Request, data *framework.Fiel return nil, "", metadata, fmt.Errorf("failed to verify the CIDR restrictions set on the role: %v", err) } if !belongs { - return nil, "", metadata, fmt.Errorf("source address unauthorized through CIDR restrictions on the role") + return nil, "", metadata, fmt.Errorf("source address %q unauthorized through CIDR restrictions on the role", req.Connection.RemoteAddr) } } @@ -199,7 +199,7 @@ func (b *backend) validateBindSecretID(req *logical.Request, roleName, secretID, } if belongs, err := cidrutil.IPBelongsToCIDRBlocksSlice(req.Connection.RemoteAddr, result.CIDRList); !belongs || err != nil { - return false, nil, fmt.Errorf("source address unauthorized through CIDR restrictions on the secret ID: %v", err) + return false, nil, fmt.Errorf("source address %q unauthorized through CIDR restrictions on the secret ID: %v", req.Connection.RemoteAddr, err) } } @@ -261,7 +261,7 @@ func (b *backend) validateBindSecretID(req *logical.Request, roleName, secretID, } if belongs, err := cidrutil.IPBelongsToCIDRBlocksSlice(req.Connection.RemoteAddr, result.CIDRList); !belongs || err != nil { - return false, nil, fmt.Errorf("source address unauthorized through CIDR restrictions on the secret ID: %v", err) + return false, nil, fmt.Errorf("source address %q unauthorized through CIDR restrictions on the secret ID: %v", req.Connection.RemoteAddr, err) } } diff --git a/helper/cidrutil/cidr_test.go b/helper/cidrutil/cidr_test.go index e220f0eae4..f6d5849c3e 100644 --- a/helper/cidrutil/cidr_test.go +++ b/helper/cidrutil/cidr_test.go @@ -14,6 +14,16 @@ func TestCIDRUtil_IPBelongsToCIDR(t *testing.T) { t.Fatalf("expected IP %q to belong to CIDR %q", ip, cidr) } + ip = "10.197.192.6" + cidr = "10.197.192.0/18" + belongs, err = IPBelongsToCIDR(ip, cidr) + if err != nil { + t.Fatal(err) + } + if !belongs { + t.Fatalf("expected IP %q to belong to CIDR %q", ip, cidr) + } + ip = "192.168.25.30" cidr = "192.168.26.30/24" belongs, err = IPBelongsToCIDR(ip, cidr) @@ -44,6 +54,17 @@ func TestCIDRUtil_IPBelongsToCIDRBlocksString(t *testing.T) { t.Fatalf("expected IP %q to belong to one of the CIDRs in %q", ip, cidrList) } + ip = "10.197.192.6" + cidrList = "1.2.3.0/8,10.197.192.0/18,10.197.193.0/24" + + belongs, err = IPBelongsToCIDRBlocksString(ip, cidrList, ",") + if err != nil { + t.Fatal(err) + } + if !belongs { + t.Fatalf("expected IP %q to belong to one of the CIDRs in %q", ip, cidrList) + } + ip = "192.168.27.29" cidrList = "172.169.100.200/18,192.168.0.0.0/16,10.10.20.20/24" From 106521b1289b24c77f291f00a2f19c238f2531aa Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 16 Feb 2017 00:55:29 -0500 Subject: [PATCH 02/30] changelog++ --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b2012eb45..ecb4e33aa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ IMPROVEMENTS: BUG FIXES: + * audit: When auditing headers use case-insensitive comparisons [GH-2362] * auth/aws-ec2: Return role period in seconds and not nanoseconds [GH-2374] * auth/okta: Fix panic if user had no local groups and/or policies set [GH-2367] From 5e5d9baabea14d45203d26928caaf64362d57e66 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 16 Feb 2017 01:04:29 -0500 Subject: [PATCH 03/30] Add Organization support to PKI backend. (#2380) Fixes #2369 --- builtin/logical/pki/backend_test.go | 29 +++++++++++++++++++ builtin/logical/pki/cert_util.go | 12 ++++++++ builtin/logical/pki/path_roles.go | 9 ++++++ website/source/docs/secrets/pki/index.html.md | 6 ++++ 4 files changed, 56 insertions(+) diff --git a/builtin/logical/pki/backend_test.go b/builtin/logical/pki/backend_test.go index 2669efcb44..4cb2c9acc9 100644 --- a/builtin/logical/pki/backend_test.go +++ b/builtin/logical/pki/backend_test.go @@ -1478,6 +1478,27 @@ func generateRoleSteps(t *testing.T, useCSRs bool) []logicaltest.TestStep { } } + getOrganizationCheck := func(role roleEntry) logicaltest.TestCheckFunc { + var certBundle certutil.CertBundle + return func(resp *logical.Response) error { + err := mapstructure.Decode(resp.Data, &certBundle) + if err != nil { + return err + } + parsedCertBundle, err := certBundle.ToParsedCertBundle() + if err != nil { + return fmt.Errorf("Error checking generated certificate: %s", err) + } + cert := parsedCertBundle.Certificate + + expected := strutil.ParseDedupAndSortStrings(role.Organization, ",") + if !reflect.DeepEqual(cert.Subject.Organization, expected) { + return fmt.Errorf("Error: returned certificate has Organization of %s but %s was specified in the role.", cert.Subject.Organization, expected) + } + return nil + } + } + // Returns a TestCheckFunc that performs various validity checks on the // returned certificate information, mostly within checkCertsAndPrivateKey getCnCheck := func(name string, role roleEntry, key crypto.Signer, usage x509.KeyUsage, extUsage x509.ExtKeyUsage, validity time.Duration) logicaltest.TestCheckFunc { @@ -1755,6 +1776,14 @@ func generateRoleSteps(t *testing.T, useCSRs bool) []logicaltest.TestStep { roleVals.OU = "foo,bar" addTests(getOuCheck(roleVals)) } + // Organization tests + { + roleVals.Organization = "system:masters" + addTests(getOrganizationCheck(roleVals)) + + roleVals.Organization = "foo,bar" + addTests(getOrganizationCheck(roleVals)) + } // IP SAN tests { issueVals.IPSANs = "127.0.0.1,::1" diff --git a/builtin/logical/pki/cert_util.go b/builtin/logical/pki/cert_util.go index 44dc388864..9a76972f37 100644 --- a/builtin/logical/pki/cert_util.go +++ b/builtin/logical/pki/cert_util.go @@ -35,6 +35,7 @@ const ( type creationBundle struct { CommonName string OU []string + Organization []string DNSNames []string EmailAddresses []string IPAddresses []net.IP @@ -581,6 +582,14 @@ func generateCreationBundle(b *backend, } } + // Set O (organization) values if specified in the role + organization := []string{} + { + if role.Organization != "" { + ou = strutil.ParseDedupAndSortStrings(role.Organization, ",") + } + } + // Read in alternate names -- DNS and email addresses dnsNames := []string{} emailAddresses := []string{} @@ -728,6 +737,7 @@ func generateCreationBundle(b *backend, creationBundle := &creationBundle{ CommonName: cn, OU: ou, + Organization: organization, DNSNames: dnsNames, EmailAddresses: emailAddresses, IPAddresses: ipAddresses, @@ -820,6 +830,7 @@ func createCertificate(creationInfo *creationBundle) (*certutil.ParsedCertBundle subject := pkix.Name{ CommonName: creationInfo.CommonName, OrganizationalUnit: creationInfo.OU, + Organization: creationInfo.Organization, } certTemplate := &x509.Certificate{ @@ -983,6 +994,7 @@ func signCertificate(creationInfo *creationBundle, subject := pkix.Name{ CommonName: creationInfo.CommonName, OrganizationalUnit: creationInfo.OU, + Organization: creationInfo.Organization, } certTemplate := &x509.Certificate{ diff --git a/builtin/logical/pki/path_roles.go b/builtin/logical/pki/path_roles.go index 6ca59051ad..879952207e 100644 --- a/builtin/logical/pki/path_roles.go +++ b/builtin/logical/pki/path_roles.go @@ -172,6 +172,13 @@ Names. Defaults to true.`, Type: framework.TypeString, Default: "", Description: `If set, the OU (OrganizationalUnit) will be set to +this value in certificates issued by this role.`, + }, + + "organization": &framework.FieldSchema{ + Type: framework.TypeString, + Default: "", + Description: `If set, the O (Organization) will be set to this value in certificates issued by this role.`, }, }, @@ -336,6 +343,7 @@ func (b *backend) pathRoleCreate( UseCSRCommonName: data.Get("use_csr_common_name").(bool), KeyUsage: data.Get("key_usage").(string), OU: data.Get("ou").(string), + Organization: data.Get("organization").(string), } if entry.KeyType == "rsa" && entry.KeyBits < 2048 { @@ -451,6 +459,7 @@ type roleEntry struct { MaxPathLength *int `json:",omitempty" structs:",omitempty"` KeyUsage string `json:"key_usage" structs:"key_usage" mapstructure:"key_usage"` OU string `json:"ou" structs:"ou" mapstructure:"ou"` + Organization string `json:"organization" structs:"organization" mapstructure:"organization"` } const pathListRolesHelpSyn = `List the existing roles in this backend` diff --git a/website/source/docs/secrets/pki/index.html.md b/website/source/docs/secrets/pki/index.html.md index 8e65d10516..985cbcdb33 100644 --- a/website/source/docs/secrets/pki/index.html.md +++ b/website/source/docs/secrets/pki/index.html.md @@ -1229,6 +1229,12 @@ subpath for interactive help output. This sets the OU (OrganizationalUnit) values in the subject field of issued certificates. This is a comma-separated string. +
  • + organization + optional + This sets the O (Organization) values in the subject field of issued + certificates. This is a comma-separated string. +
  • From ead5a34ca7c5fd45599f3fce515124e9e7e80ae7 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 16 Feb 2017 01:05:32 -0500 Subject: [PATCH 04/30] changelog++ --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecb4e33aa7..d04aefddc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ IMPROVEMENTS: [GH-2154] * audit: Support adding a configurable prefix (such as `@cee`) before each line [GH-2359] + * secret/pki: O (Organization) values can now be set to role-defined values + for issued/signed certificates [GH-2369] BUG FIXES: From 28883acc1689e1549062ae3f7c1112f18fba2e45 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 16 Feb 2017 01:32:39 -0500 Subject: [PATCH 05/30] Fix copypasta, thanks tests --- builtin/logical/pki/cert_util.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/logical/pki/cert_util.go b/builtin/logical/pki/cert_util.go index 9a76972f37..212cc57555 100644 --- a/builtin/logical/pki/cert_util.go +++ b/builtin/logical/pki/cert_util.go @@ -586,7 +586,7 @@ func generateCreationBundle(b *backend, organization := []string{} { if role.Organization != "" { - ou = strutil.ParseDedupAndSortStrings(role.Organization, ",") + organization = strutil.ParseDedupAndSortStrings(role.Organization, ",") } } From 6d49eb7b8f5883a109ecf6cb9bdfec5cc3f07d7c Mon Sep 17 00:00:00 2001 From: Frank Gevaerts Date: Thu, 16 Feb 2017 13:25:22 +0100 Subject: [PATCH 06/30] Update hsm.html.md (#2381) --- website/source/intro/vs/hsm.html.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/intro/vs/hsm.html.md b/website/source/intro/vs/hsm.html.md index 616dfb9dba..d203690cbb 100644 --- a/website/source/intro/vs/hsm.html.md +++ b/website/source/intro/vs/hsm.html.md @@ -28,7 +28,7 @@ with a significant advantage in that they conform to government-mandated compliance requirements (e.g. FIPS 140), which often require specific hardware protections and security models in addition to software. -Vault doesn't replace an HSM. Instead, they can be complimentary; a compliant +Vault doesn't replace an HSM. Instead, they can be complementary; a compliant HSM can protect Vault's master key to help Vault comply with regulatory requirements, and Vault can provide easy client APIs for tasks such as encryption and decryption. From 513f8b918d7c414788d5d7ac29074e9817e7ba8b Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 16 Feb 2017 11:37:27 -0500 Subject: [PATCH 07/30] Add WithOptions methods to audit/auth enabling (#2383) --- api/sys_audit.go | 24 ++++++++++++++---- api/sys_auth.go | 21 ++++++++++++--- api/sys_mounts.go | 2 ++ command/audit_disable_test.go | 40 +++++++++++++++++++++++++++++ command/audit_enable.go | 13 +++++++++- command/audit_list.go | 9 ++++--- command/audit_list_test.go | 7 ++++- command/auth.go | 8 ++++-- command/auth_disable_test.go | 48 +++++++++++++++++++++++++++++++++++ command/auth_enable.go | 12 ++++++++- 10 files changed, 167 insertions(+), 17 deletions(-) diff --git a/api/sys_audit.go b/api/sys_audit.go index 1ffdef880f..89f2141664 100644 --- a/api/sys_audit.go +++ b/api/sys_audit.go @@ -3,6 +3,7 @@ package api import ( "fmt" + "github.com/fatih/structs" "github.com/mitchellh/mapstructure" ) @@ -71,13 +72,18 @@ func (c *Sys) ListAudit() (map[string]*Audit, error) { return mounts, nil } +// DEPRECATED: Use EnableAuditWithOptions instead func (c *Sys) EnableAudit( path string, auditType string, desc string, opts map[string]string) error { - body := map[string]interface{}{ - "type": auditType, - "description": desc, - "options": opts, - } + return c.EnableAuditWithOptions(path, &EnableAuditOptions{ + Type: auditType, + Description: desc, + Options: opts, + }) +} + +func (c *Sys) EnableAuditWithOptions(path string, options *EnableAuditOptions) error { + body := structs.Map(options) r := c.c.NewRequest("PUT", fmt.Sprintf("/v1/sys/audit/%s", path)) if err := r.SetJSONBody(body); err != nil { @@ -106,9 +112,17 @@ func (c *Sys) DisableAudit(path string) error { // individually documented because the map almost directly to the raw HTTP API // documentation. Please refer to that documentation for more details. +type EnableAuditOptions struct { + Type string `json:"type" structs:"type"` + Description string `json:"description" structs:"description"` + Options map[string]string `json:"options" structs:"options"` + Local bool `json:"local" structs:"local"` +} + type Audit struct { Path string Type string Description string Options map[string]string + Local bool } diff --git a/api/sys_auth.go b/api/sys_auth.go index 1940e84172..f9f3c8c2cc 100644 --- a/api/sys_auth.go +++ b/api/sys_auth.go @@ -3,6 +3,7 @@ package api import ( "fmt" + "github.com/fatih/structs" "github.com/mitchellh/mapstructure" ) @@ -42,11 +43,16 @@ func (c *Sys) ListAuth() (map[string]*AuthMount, error) { return mounts, nil } +// DEPRECATED: Use EnableAuthWithOptions instead func (c *Sys) EnableAuth(path, authType, desc string) error { - body := map[string]string{ - "type": authType, - "description": desc, - } + return c.EnableAuthWithOptions(path, &EnableAuthOptions{ + Type: authType, + Description: desc, + }) +} + +func (c *Sys) EnableAuthWithOptions(path string, options *EnableAuthOptions) error { + body := structs.Map(options) r := c.c.NewRequest("POST", fmt.Sprintf("/v1/sys/auth/%s", path)) if err := r.SetJSONBody(body); err != nil { @@ -75,10 +81,17 @@ func (c *Sys) DisableAuth(path string) error { // individually documentd because the map almost directly to the raw HTTP API // documentation. Please refer to that documentation for more details. +type EnableAuthOptions struct { + Type string `json:"type" structs:"type"` + Description string `json:"description" structs:"description"` + Local bool `json:"local" structs:"local"` +} + type AuthMount struct { Type string `json:"type" structs:"type" mapstructure:"type"` Description string `json:"description" structs:"description" mapstructure:"description"` Config AuthConfigOutput `json:"config" structs:"config" mapstructure:"config"` + Local bool `json:"local" structs:"local" mapstructure:"local"` } type AuthConfigOutput struct { diff --git a/api/sys_mounts.go b/api/sys_mounts.go index ca5e42707a..768e09fd61 100644 --- a/api/sys_mounts.go +++ b/api/sys_mounts.go @@ -123,6 +123,7 @@ type MountInput struct { Type string `json:"type" structs:"type"` Description string `json:"description" structs:"description"` Config MountConfigInput `json:"config" structs:"config"` + Local bool `json:"local" structs:"local"` } type MountConfigInput struct { @@ -134,6 +135,7 @@ type MountOutput struct { Type string `json:"type" structs:"type"` Description string `json:"description" structs:"description"` Config MountConfigOutput `json:"config" structs:"config"` + Local bool `json:"local" structs:"local"` } type MountConfigOutput struct { diff --git a/command/audit_disable_test.go b/command/audit_disable_test.go index 012bb20c5e..500ee9ccb1 100644 --- a/command/audit_disable_test.go +++ b/command/audit_disable_test.go @@ -3,6 +3,7 @@ package command import ( "testing" + "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/http" "github.com/hashicorp/vault/meta" "github.com/hashicorp/vault/vault" @@ -44,3 +45,42 @@ func TestAuditDisable(t *testing.T) { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) } } + +func TestAuditDisableWithOptions(t *testing.T) { + core, _, token := vault.TestCoreUnsealed(t) + ln, addr := http.TestServer(t, core) + defer ln.Close() + + ui := new(cli.MockUi) + c := &AuditDisableCommand{ + Meta: meta.Meta{ + ClientToken: token, + Ui: ui, + }, + } + + args := []string{ + "-address", addr, + "noop", + } + + // Run once to get the client + c.Run(args) + + // Get the client + client, err := c.Client() + if err != nil { + t.Fatalf("err: %#v", err) + } + if err := client.Sys().EnableAuditWithOptions("noop", &api.EnableAuditOptions{ + Type: "noop", + Description: "noop", + }); err != nil { + t.Fatalf("err: %#v", err) + } + + // Run again + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } +} diff --git a/command/audit_enable.go b/command/audit_enable.go index 7178c60423..3293c792f9 100644 --- a/command/audit_enable.go +++ b/command/audit_enable.go @@ -6,6 +6,7 @@ import ( "os" "strings" + "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/helper/kv-builder" "github.com/hashicorp/vault/meta" "github.com/mitchellh/mapstructure" @@ -21,9 +22,11 @@ type AuditEnableCommand struct { func (c *AuditEnableCommand) Run(args []string) int { var desc, path string + var local bool flags := c.Meta.FlagSet("audit-enable", meta.FlagSetDefault) flags.StringVar(&desc, "description", "", "") flags.StringVar(&path, "path", "", "") + flags.BoolVar(&local, "local", false, "") flags.Usage = func() { c.Ui.Error(c.Help()) } if err := flags.Parse(args); err != nil { return 1 @@ -68,7 +71,12 @@ func (c *AuditEnableCommand) Run(args []string) int { return 1 } - err = client.Sys().EnableAudit(path, auditType, desc, opts) + err = client.Sys().EnableAuditWithOptions(path, &api.EnableAuditOptions{ + Type: auditType, + Description: desc, + Options: opts, + Local: local, + }) if err != nil { c.Ui.Error(fmt.Sprintf( "Error enabling audit backend: %s", err)) @@ -113,6 +121,9 @@ Audit Enable Options: is purely for referencing this audit backend. By default this will be the backend type. + -local Mark the mount as a local mount. Local mounts + are not replicated nor (if a secondary) + removed by replication. ` return strings.TrimSpace(helpText) } diff --git a/command/audit_list.go b/command/audit_list.go index 20c070f132..b9914eb929 100644 --- a/command/audit_list.go +++ b/command/audit_list.go @@ -48,16 +48,19 @@ func (c *AuditListCommand) Run(args []string) int { } sort.Strings(paths) - columns := []string{"Path | Type | Description | Options"} + columns := []string{"Path | Type | Description | Replication Behavior | Options"} for _, path := range paths { audit := audits[path] opts := make([]string, 0, len(audit.Options)) for k, v := range audit.Options { opts = append(opts, k+"="+v) } - + replicatedBehavior := "replicated" + if audit.Local { + replicatedBehavior = "local" + } columns = append(columns, fmt.Sprintf( - "%s | %s | %s | %s", audit.Path, audit.Type, audit.Description, strings.Join(opts, " "))) + "%s | %s | %s | %s | %s", audit.Path, audit.Type, audit.Description, replicatedBehavior, strings.Join(opts, " "))) } c.Ui.Output(columnize.SimpleFormat(columns)) diff --git a/command/audit_list_test.go b/command/audit_list_test.go index 0f35aebcce..01d4f83ed0 100644 --- a/command/audit_list_test.go +++ b/command/audit_list_test.go @@ -3,6 +3,7 @@ package command import ( "testing" + "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/http" "github.com/hashicorp/vault/meta" "github.com/hashicorp/vault/vault" @@ -34,7 +35,11 @@ func TestAuditList(t *testing.T) { if err != nil { t.Fatalf("err: %#v", err) } - if err := client.Sys().EnableAudit("foo", "noop", "", nil); err != nil { + if err := client.Sys().EnableAuditWithOptions("foo", &api.EnableAuditOptions{ + Type: "noop", + Description: "noop", + Options: nil, + }); err != nil { t.Fatalf("err: %#v", err) } diff --git a/command/auth.go b/command/auth.go index 1dc15fb3c3..6ea2a0fe9c 100644 --- a/command/auth.go +++ b/command/auth.go @@ -281,7 +281,7 @@ func (c *AuthCommand) listMethods() int { } sort.Strings(paths) - columns := []string{"Path | Type | Default TTL | Max TTL | Description"} + columns := []string{"Path | Type | Default TTL | Max TTL | Replication Behavior | Description"} for _, path := range paths { auth := auth[path] defTTL := "system" @@ -292,8 +292,12 @@ func (c *AuthCommand) listMethods() int { if auth.Config.MaxLeaseTTL != 0 { maxTTL = strconv.Itoa(auth.Config.MaxLeaseTTL) } + replicatedBehavior := "replicated" + if auth.Local { + replicatedBehavior = "local" + } columns = append(columns, fmt.Sprintf( - "%s | %s | %s | %s | %s", path, auth.Type, defTTL, maxTTL, auth.Description)) + "%s | %s | %s | %s | %s | %s", path, auth.Type, defTTL, maxTTL, replicatedBehavior, auth.Description)) } c.Ui.Output(columnize.SimpleFormat(columns)) diff --git a/command/auth_disable_test.go b/command/auth_disable_test.go index 65cb48f61b..fb2b91fb23 100644 --- a/command/auth_disable_test.go +++ b/command/auth_disable_test.go @@ -3,6 +3,7 @@ package command import ( "testing" + "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/http" "github.com/hashicorp/vault/meta" "github.com/hashicorp/vault/vault" @@ -52,3 +53,50 @@ func TestAuthDisable(t *testing.T) { t.Fatal("should not have noop mount") } } + +func TestAuthDisableWithOptions(t *testing.T) { + core, _, token := vault.TestCoreUnsealed(t) + ln, addr := http.TestServer(t, core) + defer ln.Close() + + ui := new(cli.MockUi) + c := &AuthDisableCommand{ + Meta: meta.Meta{ + ClientToken: token, + Ui: ui, + }, + } + + args := []string{ + "-address", addr, + "noop", + } + + // Run the command once to setup the client, it will fail + c.Run(args) + + client, err := c.Client() + if err != nil { + t.Fatalf("err: %s", err) + } + + if err := client.Sys().EnableAuthWithOptions("noop", &api.EnableAuthOptions{ + Type: "noop", + Description: "", + }); err != nil { + t.Fatalf("err: %#v", err) + } + + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + mounts, err := client.Sys().ListAuth() + if err != nil { + t.Fatalf("err: %s", err) + } + + if _, ok := mounts["noop"]; ok { + t.Fatal("should not have noop mount") + } +} diff --git a/command/auth_enable.go b/command/auth_enable.go index 882d97985d..c7b8422ed3 100644 --- a/command/auth_enable.go +++ b/command/auth_enable.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/meta" ) @@ -14,9 +15,11 @@ type AuthEnableCommand struct { func (c *AuthEnableCommand) Run(args []string) int { var description, path string + var local bool flags := c.Meta.FlagSet("auth-enable", meta.FlagSetDefault) flags.StringVar(&description, "description", "", "") flags.StringVar(&path, "path", "", "") + flags.BoolVar(&local, "local", false, "") flags.Usage = func() { c.Ui.Error(c.Help()) } if err := flags.Parse(args); err != nil { return 1 @@ -44,7 +47,11 @@ func (c *AuthEnableCommand) Run(args []string) int { return 2 } - if err := client.Sys().EnableAuth(path, authType, description); err != nil { + if err := client.Sys().EnableAuthWithOptions(path, &api.EnableAuthOptions{ + Type: authType, + Description: description, + Local: local, + }); err != nil { c.Ui.Error(fmt.Sprintf( "Error: %s", err)) return 2 @@ -82,6 +89,9 @@ Auth Enable Options: to the type of the mount. This will make the auth provider available at "/auth/" + -local Mark the mount as a local mount. Local mounts + are not replicated nor (if a secondary) + removed by replication. ` return strings.TrimSpace(helpText) } From 64d63ba55aeb49a4d5b71f664a3ca96283bb0baa Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 16 Feb 2017 13:09:53 -0500 Subject: [PATCH 08/30] Add some repcluster handling to audit and add some tests (#2384) * Add some repcluster handling to audit and add some tests * Fix incorrect assumption about nil auth --- audit/format.go | 121 ++++++++++++++++++++-------------- audit/format_test.go | 55 ++++++++++++++++ builtin/audit/file/backend.go | 13 ++-- logical/request.go | 6 +- 4 files changed, 141 insertions(+), 54 deletions(-) create mode 100644 audit/format_test.go diff --git a/audit/format.go b/audit/format.go index 4eb4f22824..86b00f8a25 100644 --- a/audit/format.go +++ b/audit/format.go @@ -27,7 +27,11 @@ func (f *AuditFormatter) FormatRequest( config FormatterConfig, auth *logical.Auth, req *logical.Request, - err error) error { + inErr error) error { + + if req == nil { + return fmt.Errorf("request to request-audit a nil request") + } if w == nil { return fmt.Errorf("writer for audit request is nil") @@ -49,22 +53,26 @@ func (f *AuditFormatter) FormatRequest( }() } - // Copy the structures - cp, err := copystructure.Copy(auth) - if err != nil { - return err + // Copy the auth structure + if auth != nil { + cp, err := copystructure.Copy(auth) + if err != nil { + return err + } + auth = cp.(*logical.Auth) } - auth = cp.(*logical.Auth) - cp, err = copystructure.Copy(req) + cp, err := copystructure.Copy(req) if err != nil { return err } req = cp.(*logical.Request) // Hash any sensitive information - if err := Hash(config.Salt, auth); err != nil { - return err + if auth != nil { + if err := Hash(config.Salt, auth); err != nil { + return err + } } // Cache and restore accessor in the request @@ -85,8 +93,8 @@ func (f *AuditFormatter) FormatRequest( auth = new(logical.Auth) } var errString string - if err != nil { - errString = err.Error() + if inErr != nil { + errString = inErr.Error() } reqEntry := &AuditRequestEntry{ @@ -107,6 +115,7 @@ func (f *AuditFormatter) FormatRequest( Path: req.Path, Data: req.Data, RemoteAddr: getRemoteAddr(req), + ReplicationCluster: req.ReplicationCluster, Headers: req.Headers, }, } @@ -128,7 +137,11 @@ func (f *AuditFormatter) FormatResponse( auth *logical.Auth, req *logical.Request, resp *logical.Response, - err error) error { + inErr error) error { + + if req == nil { + return fmt.Errorf("request to response-audit a nil request") + } if w == nil { return fmt.Errorf("writer for audit request is nil") @@ -150,37 +163,43 @@ func (f *AuditFormatter) FormatResponse( }() } - // Copy the structure - cp, err := copystructure.Copy(auth) - if err != nil { - return err + // Copy the auth structure + if auth != nil { + cp, err := copystructure.Copy(auth) + if err != nil { + return err + } + auth = cp.(*logical.Auth) } - auth = cp.(*logical.Auth) - cp, err = copystructure.Copy(req) + cp, err := copystructure.Copy(req) if err != nil { return err } req = cp.(*logical.Request) - cp, err = copystructure.Copy(resp) - if err != nil { - return err + if resp != nil { + cp, err := copystructure.Copy(resp) + if err != nil { + return err + } + resp = cp.(*logical.Response) } - resp = cp.(*logical.Response) // Hash any sensitive information // Cache and restore accessor in the auth - var accessor, wrappedAccessor string - if !config.HMACAccessor && auth != nil && auth.Accessor != "" { - accessor = auth.Accessor - } - if err := Hash(config.Salt, auth); err != nil { - return err - } - if accessor != "" { - auth.Accessor = accessor + if auth != nil { + var accessor string + if !config.HMACAccessor && auth.Accessor != "" { + accessor = auth.Accessor + } + if err := Hash(config.Salt, auth); err != nil { + return err + } + if accessor != "" { + auth.Accessor = accessor + } } // Cache and restore accessor in the request @@ -196,21 +215,23 @@ func (f *AuditFormatter) FormatResponse( } // Cache and restore accessor in the response - accessor = "" - if !config.HMACAccessor && resp != nil && resp.Auth != nil && resp.Auth.Accessor != "" { - accessor = resp.Auth.Accessor - } - if !config.HMACAccessor && resp != nil && resp.WrapInfo != nil && resp.WrapInfo.WrappedAccessor != "" { - wrappedAccessor = resp.WrapInfo.WrappedAccessor - } - if err := Hash(config.Salt, resp); err != nil { - return err - } - if accessor != "" { - resp.Auth.Accessor = accessor - } - if wrappedAccessor != "" { - resp.WrapInfo.WrappedAccessor = wrappedAccessor + if resp != nil { + var accessor, wrappedAccessor string + if !config.HMACAccessor && resp != nil && resp.Auth != nil && resp.Auth.Accessor != "" { + accessor = resp.Auth.Accessor + } + if !config.HMACAccessor && resp != nil && resp.WrapInfo != nil && resp.WrapInfo.WrappedAccessor != "" { + wrappedAccessor = resp.WrapInfo.WrappedAccessor + } + if err := Hash(config.Salt, resp); err != nil { + return err + } + if accessor != "" { + resp.Auth.Accessor = accessor + } + if wrappedAccessor != "" { + resp.WrapInfo.WrappedAccessor = wrappedAccessor + } } } @@ -222,8 +243,8 @@ func (f *AuditFormatter) FormatResponse( resp = new(logical.Response) } var errString string - if err != nil { - errString = err.Error() + if inErr != nil { + errString = inErr.Error() } var respAuth *AuditAuth @@ -276,6 +297,7 @@ func (f *AuditFormatter) FormatResponse( Path: req.Path, Data: req.Data, RemoteAddr: getRemoteAddr(req), + ReplicationCluster: req.ReplicationCluster, Headers: req.Headers, }, @@ -312,14 +334,15 @@ type AuditRequestEntry struct { type AuditResponseEntry struct { Time string `json:"time,omitempty"` Type string `json:"type"` - Error string `json:"error"` Auth AuditAuth `json:"auth"` Request AuditRequest `json:"request"` Response AuditResponse `json:"response"` + Error string `json:"error"` } type AuditRequest struct { ID string `json:"id"` + ReplicationCluster string `json:"replication_cluster,omitempty"` Operation logical.Operation `json:"operation"` ClientToken string `json:"client_token"` ClientTokenAccessor string `json:"client_token_accessor"` diff --git a/audit/format_test.go b/audit/format_test.go new file mode 100644 index 0000000000..6a6425b3a4 --- /dev/null +++ b/audit/format_test.go @@ -0,0 +1,55 @@ +package audit + +import ( + "io" + "io/ioutil" + "testing" + + "github.com/hashicorp/vault/helper/salt" + "github.com/hashicorp/vault/logical" +) + +type noopFormatWriter struct { +} + +func (n *noopFormatWriter) WriteRequest(_ io.Writer, _ *AuditRequestEntry) error { + return nil +} + +func (n *noopFormatWriter) WriteResponse(_ io.Writer, _ *AuditResponseEntry) error { + return nil +} + +func TestFormatRequestErrors(t *testing.T) { + salter, _ := salt.NewSalt(nil, nil) + config := FormatterConfig{ + Salt: salter, + } + formatter := AuditFormatter{ + AuditFormatWriter: &noopFormatWriter{}, + } + + if err := formatter.FormatRequest(ioutil.Discard, config, nil, nil, nil); err == nil { + t.Fatal("expected error due to nil request") + } + if err := formatter.FormatRequest(nil, config, nil, &logical.Request{}, nil); err == nil { + t.Fatal("expected error due to nil writer") + } +} + +func TestFormatResponseErrors(t *testing.T) { + salter, _ := salt.NewSalt(nil, nil) + config := FormatterConfig{ + Salt: salter, + } + formatter := AuditFormatter{ + AuditFormatWriter: &noopFormatWriter{}, + } + + if err := formatter.FormatResponse(ioutil.Discard, config, nil, nil, nil, nil); err == nil { + t.Fatal("expected error due to nil request") + } + if err := formatter.FormatResponse(nil, config, nil, &logical.Request{}, nil, nil); err == nil { + t.Fatal("expected error due to nil writer") + } +} diff --git a/builtin/audit/file/backend.go b/builtin/audit/file/backend.go index 359d9242ef..cc2cfe5540 100644 --- a/builtin/audit/file/backend.go +++ b/builtin/audit/file/backend.go @@ -157,10 +157,15 @@ func (b *Backend) open() error { return err } - // Change the file mode in case the log file already existed - err = os.Chmod(b.path, b.mode) - if err != nil { - return err + // Change the file mode in case the log file already existed. We special + // case /dev/null since we can't chmod it + switch b.path { + case "/dev/null": + default: + err = os.Chmod(b.path, b.mode) + if err != nil { + return err + } } return nil diff --git a/logical/request.go b/logical/request.go index f352b9ea89..f7f49e2d4c 100644 --- a/logical/request.go +++ b/logical/request.go @@ -25,6 +25,10 @@ type Request struct { // Id is the uuid associated with each request ID string `json:"id" structs:"id" mapstructure:"id"` + // If set, the name given to the replication secondary where this request + // originated + ReplicationCluster string `json:"replication_cluster" structs:"replication_cluster", mapstructure:"replication_cluster"` + // Operation is the requested operation type Operation Operation `json:"operation" structs:"operation" mapstructure:"operation"` @@ -38,7 +42,7 @@ type Request struct { Data map[string]interface{} `json:"map" structs:"data" mapstructure:"data"` // Storage can be used to durably store and retrieve state. - Storage Storage `json:"storage" structs:"storage" mapstructure:"storage"` + Storage Storage `json:"-"` // Secret will be non-nil only for Revoke and Renew operations // to represent the secret that was returned prior. From bc16792ffd2820d504712266d68d99099a3da555 Mon Sep 17 00:00:00 2001 From: Brian Kassouf Date: Thu, 16 Feb 2017 10:16:06 -0800 Subject: [PATCH 09/30] Load leases into the expiration manager in parallel (#2370) * Add a benchmark for exiration.Restore * Add benchmarks for consul Restore functions * Add a parallel version of expiration.Restore * remove debug code * Up the MaxIdleConnsPerHost * Add tests for etcd * Return errors and ensure go routines are exited * Refactor inmem benchmark * Add s3 bench and refactor a bit * Few tweaks * Fix race with waitgroup.Add() * Fix waitgroup race condition * Move wait above the info log * Add helper/consts package to store consts that are needed in cyclic packages * Remove not used benchmarks --- helper/consts/consts.go | 7 ++ physical/consul.go | 7 +- vault/barrier_aes_gcm_test.go | 2 +- vault/expiration.go | 125 ++++++++++++++++++++++++++-------- vault/expiration_test.go | 110 +++++++++++++++++++++++++++++- vault/testing.go | 103 +++++++++++++++++++++------- 6 files changed, 298 insertions(+), 56 deletions(-) create mode 100644 helper/consts/consts.go diff --git a/helper/consts/consts.go b/helper/consts/consts.go new file mode 100644 index 0000000000..2ec952b2b9 --- /dev/null +++ b/helper/consts/consts.go @@ -0,0 +1,7 @@ +package consts + +const ( + // ExpirationRestoreWorkerCount specifies the numer of workers to use while + // restoring leases into the expiration manager + ExpirationRestoreWorkerCount = 64 +) diff --git a/physical/consul.go b/physical/consul.go index 9f069e4862..c5c9f6a263 100644 --- a/physical/consul.go +++ b/physical/consul.go @@ -21,6 +21,7 @@ import ( "github.com/hashicorp/consul/lib" "github.com/hashicorp/errwrap" "github.com/hashicorp/go-cleanhttp" + "github.com/hashicorp/vault/helper/consts" "github.com/hashicorp/vault/helper/strutil" "github.com/hashicorp/vault/helper/tlsutil" ) @@ -154,6 +155,10 @@ func newConsulBackend(conf map[string]string, logger log.Logger) (Backend, error // Configure the client consulConf := api.DefaultConfig() + // Set MaxIdleConnsPerHost to the number of processes used in expiration.Restore + tr := cleanhttp.DefaultPooledTransport() + tr.MaxIdleConnsPerHost = consts.ExpirationRestoreWorkerCount + consulConf.HttpClient.Transport = tr if addr, ok := conf["address"]; ok { consulConf.Address = addr @@ -179,7 +184,7 @@ func newConsulBackend(conf map[string]string, logger log.Logger) (Backend, error } transport := cleanhttp.DefaultPooledTransport() - transport.MaxIdleConnsPerHost = 4 + transport.MaxIdleConnsPerHost = consts.ExpirationRestoreWorkerCount transport.TLSClientConfig = tlsClientConfig consulConf.HttpClient.Transport = transport logger.Debug("physical/consul: configured TLS") diff --git a/vault/barrier_aes_gcm_test.go b/vault/barrier_aes_gcm_test.go index b1957b3092..3814139bf5 100644 --- a/vault/barrier_aes_gcm_test.go +++ b/vault/barrier_aes_gcm_test.go @@ -15,7 +15,7 @@ var ( ) // mockBarrier returns a physical backend, security barrier, and master key -func mockBarrier(t *testing.T) (physical.Backend, SecurityBarrier, []byte) { +func mockBarrier(t testing.TB) (physical.Backend, SecurityBarrier, []byte) { inm := physical.NewInmem(logger) b, err := NewAESGCMBarrier(inm) diff --git a/vault/expiration.go b/vault/expiration.go index 84420c6d60..87fceaba67 100644 --- a/vault/expiration.go +++ b/vault/expiration.go @@ -12,6 +12,7 @@ import ( log "github.com/mgutz/logxi/v1" "github.com/hashicorp/go-uuid" + "github.com/hashicorp/vault/helper/consts" "github.com/hashicorp/vault/helper/jsonutil" "github.com/hashicorp/vault/logical" ) @@ -125,46 +126,114 @@ func (m *ExpirationManager) Restore() error { if err != nil { return fmt.Errorf("failed to scan for leases: %v", err) } - m.logger.Debug("expiration: leases collected", "num_existing", len(existing)) - // Restore each key - for i, leaseID := range existing { - if i%500 == 0 { - m.logger.Trace("expiration: leases loading", "progress", i) - } - // Load the entry - le, err := m.loadEntry(leaseID) - if err != nil { - return err - } + // Make the channels used for the worker pool + broker := make(chan string) + quit := make(chan bool) + // Buffer these channels to prevent deadlocks + errs := make(chan error, len(existing)) + result := make(chan *leaseEntry, len(existing)) - // If there is no entry, nothing to restore - if le == nil { - continue - } + // Use a wait group + wg := &sync.WaitGroup{} - // If there is no expiry time, don't do anything - if le.ExpireTime.IsZero() { - continue - } + // Create 64 workers to distribute work to + for i := 0; i < consts.ExpirationRestoreWorkerCount; i++ { + wg.Add(1) + go func() { + defer wg.Done() - // Determine the remaining time to expiration - expires := le.ExpireTime.Sub(time.Now()) - if expires <= 0 { - expires = minRevokeDelay - } + for { + select { + case leaseID, ok := <-broker: + // broker has been closed, we are done + if !ok { + return + } - // Setup revocation timer - m.pending[le.LeaseID] = time.AfterFunc(expires, func() { - m.expireID(le.LeaseID) - }) + le, err := m.loadEntry(leaseID) + if err != nil { + errs <- err + continue + } + + // Write results out to the result channel + result <- le + + // quit early + case <-quit: + return + } + } + }() } + + // Distribute the collected keys to the workers in a go routine + wg.Add(1) + go func() { + defer wg.Done() + for i, leaseID := range existing { + if i%500 == 0 { + m.logger.Trace("expiration: leases loading", "progress", i) + } + + select { + case <-quit: + return + + default: + broker <- leaseID + } + } + + // Close the broker, causing worker routines to exit + close(broker) + }() + + // Restore each key by pulling from the result chan + for i := 0; i < len(existing); i++ { + select { + case err := <-errs: + // Close all go routines + close(quit) + + return err + + case le := <-result: + + // If there is no entry, nothing to restore + if le == nil { + continue + } + + // If there is no expiry time, don't do anything + if le.ExpireTime.IsZero() { + continue + } + + // Determine the remaining time to expiration + expires := le.ExpireTime.Sub(time.Now()) + if expires <= 0 { + expires = minRevokeDelay + } + + // Setup revocation timer + m.pending[le.LeaseID] = time.AfterFunc(expires, func() { + m.expireID(le.LeaseID) + }) + } + } + + // Let all go routines finish + wg.Wait() + if len(m.pending) > 0 { if m.logger.IsInfo() { m.logger.Info("expire: leases restored", "restored_lease_count", len(m.pending)) } } + return nil } diff --git a/vault/expiration_test.go b/vault/expiration_test.go index 2ae8bd6650..b8255b3e9d 100644 --- a/vault/expiration_test.go +++ b/vault/expiration_test.go @@ -2,23 +2,131 @@ package vault import ( "fmt" + "os" "reflect" "sort" "strings" + "sync" "testing" "time" "github.com/hashicorp/go-uuid" + "github.com/hashicorp/vault/helper/logformat" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" + "github.com/hashicorp/vault/physical" + log "github.com/mgutz/logxi/v1" +) + +var ( + testImagePull sync.Once ) // mockExpiration returns a mock expiration manager -func mockExpiration(t *testing.T) *ExpirationManager { +func mockExpiration(t testing.TB) *ExpirationManager { _, ts, _, _ := TestCoreWithTokenStore(t) return ts.expiration } +func mockBackendExpiration(t testing.TB, backend physical.Backend) (*Core, *ExpirationManager) { + c, ts, _, _ := TestCoreWithBackendTokenStore(t, backend) + return c, ts.expiration +} + +func BenchmarkExpiration_Restore_Etcd(b *testing.B) { + addr := os.Getenv("PHYSICAL_BACKEND_BENCHMARK_ADDR") + randPath := fmt.Sprintf("vault-%d/", time.Now().Unix()) + + logger := logformat.NewVaultLogger(log.LevelTrace) + physicalBackend, err := physical.NewBackend("etcd", logger, map[string]string{ + "address": addr, + "path": randPath, + "max_parallel": "256", + }) + if err != nil { + b.Fatalf("err: %s", err) + } + + benchmarkExpirationBackend(b, physicalBackend, 10000) // 10,000 leases +} + +func BenchmarkExpiration_Restore_Consul(b *testing.B) { + addr := os.Getenv("PHYSICAL_BACKEND_BENCHMARK_ADDR") + randPath := fmt.Sprintf("vault-%d/", time.Now().Unix()) + + logger := logformat.NewVaultLogger(log.LevelTrace) + physicalBackend, err := physical.NewBackend("consul", logger, map[string]string{ + "address": addr, + "path": randPath, + "max_parallel": "256", + }) + if err != nil { + b.Fatalf("err: %s", err) + } + + benchmarkExpirationBackend(b, physicalBackend, 10000) // 10,000 leases +} + +func BenchmarkExpiration_Restore_InMem(b *testing.B) { + logger := logformat.NewVaultLogger(log.LevelTrace) + benchmarkExpirationBackend(b, physical.NewInmem(logger), 100000) // 100,000 Leases +} + +func benchmarkExpirationBackend(b *testing.B, physicalBackend physical.Backend, numLeases int) { + c, exp := mockBackendExpiration(b, physicalBackend) + noop := &NoopBackend{} + view := NewBarrierView(c.barrier, "logical/") + meUUID, err := uuid.GenerateUUID() + if err != nil { + b.Fatal(err) + } + exp.router.Mount(noop, "prod/aws/", &MountEntry{UUID: meUUID}, view) + + // Register fake leases + for i := 0; i < numLeases; i++ { + pathUUID, err := uuid.GenerateUUID() + if err != nil { + b.Fatal(err) + } + + req := &logical.Request{ + Operation: logical.ReadOperation, + Path: "prod/aws/" + pathUUID, + } + resp := &logical.Response{ + Secret: &logical.Secret{ + LeaseOptions: logical.LeaseOptions{ + TTL: 400 * time.Second, + }, + }, + Data: map[string]interface{}{ + "access_key": "xyz", + "secret_key": "abcd", + }, + } + _, err = exp.Register(req, resp) + if err != nil { + b.Fatalf("err: %v", err) + } + } + + // Stop everything + err = exp.Stop() + if err != nil { + b.Fatalf("err: %v", err) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + err = exp.Restore() + // Restore + if err != nil { + b.Fatalf("err: %v", err) + } + } + b.StopTimer() +} + func TestExpiration_Restore(t *testing.T) { exp := mockExpiration(t) noop := &NoopBackend{} diff --git a/vault/testing.go b/vault/testing.go index b287941a99..6b7706ce28 100644 --- a/vault/testing.go +++ b/vault/testing.go @@ -67,19 +67,37 @@ oOyBJU/HMVvBfv4g+OVFLVgSwwm6owwsouZ0+D/LasbuHqYyqYqdyPJQYzWA2Y+F ) // TestCore returns a pure in-memory, uninitialized core for testing. -func TestCore(t *testing.T) *Core { +func TestCore(t testing.TB) *Core { return TestCoreWithSeal(t, nil) } // TestCoreNewSeal returns an in-memory, ininitialized core with the new seal // configuration. -func TestCoreNewSeal(t *testing.T) *Core { +func TestCoreNewSeal(t testing.TB) *Core { return TestCoreWithSeal(t, &TestSeal{}) } // TestCoreWithSeal returns a pure in-memory, uninitialized core with the // specified seal for testing. -func TestCoreWithSeal(t *testing.T, testSeal Seal) *Core { +func TestCoreWithSeal(t testing.TB, testSeal Seal) *Core { + logger := logformat.NewVaultLogger(log.LevelTrace) + physicalBackend := physical.NewInmem(logger) + + conf := testCoreConfig(t, physicalBackend, logger) + + if testSeal != nil { + conf.Seal = testSeal + } + + c, err := NewCore(conf) + if err != nil { + t.Fatalf("err: %s", err) + } + + return c +} + +func testCoreConfig(t testing.TB, physicalBackend physical.Backend, logger log.Logger) *CoreConfig { noopAudits := map[string]audit.Factory{ "noop": func(config *audit.BackendConfig) (audit.Backend, error) { view := &logical.InmemStorage{} @@ -118,9 +136,6 @@ func TestCoreWithSeal(t *testing.T, testSeal Seal) *Core { logicalBackends[backendName] = backendFactory } - logger := logformat.NewVaultLogger(log.LevelTrace) - - physicalBackend := physical.NewInmem(logger) conf := &CoreConfig{ Physical: physicalBackend, AuditBackends: noopAudits, @@ -129,25 +144,17 @@ func TestCoreWithSeal(t *testing.T, testSeal Seal) *Core { DisableMlock: true, Logger: logger, } - if testSeal != nil { - conf.Seal = testSeal - } - c, err := NewCore(conf) - if err != nil { - t.Fatalf("err: %s", err) - } - - return c + return conf } // TestCoreInit initializes the core with a single key, and returns // the key that must be used to unseal the core and a root token. -func TestCoreInit(t *testing.T, core *Core) ([][]byte, string) { +func TestCoreInit(t testing.TB, core *Core) ([][]byte, string) { return TestCoreInitClusterWrapperSetup(t, core, nil, func() (http.Handler, http.Handler) { return nil, nil }) } -func TestCoreInitClusterWrapperSetup(t *testing.T, core *Core, clusterAddrs []*net.TCPAddr, handlerSetupFunc func() (http.Handler, http.Handler)) ([][]byte, string) { +func TestCoreInitClusterWrapperSetup(t testing.TB, core *Core, clusterAddrs []*net.TCPAddr, handlerSetupFunc func() (http.Handler, http.Handler)) ([][]byte, string) { core.SetClusterListenerAddrs(clusterAddrs) core.SetClusterSetupFuncs(handlerSetupFunc) result, err := core.Initialize(&InitParams{ @@ -155,7 +162,10 @@ func TestCoreInitClusterWrapperSetup(t *testing.T, core *Core, clusterAddrs []*n SecretShares: 3, SecretThreshold: 3, }, - RecoveryConfig: nil, + RecoveryConfig: &SealConfig{ + SecretShares: 3, + SecretThreshold: 3, + }, }) if err != nil { t.Fatalf("err: %s", err) @@ -170,7 +180,7 @@ func TestCoreUnseal(core *Core, key []byte) (bool, error) { // TestCoreUnsealed returns a pure in-memory core that is already // initialized and unsealed. -func TestCoreUnsealed(t *testing.T) (*Core, [][]byte, string) { +func TestCoreUnsealed(t testing.TB) (*Core, [][]byte, string) { core := TestCore(t) keys, token := TestCoreInit(t, core) for _, key := range keys { @@ -190,11 +200,35 @@ func TestCoreUnsealed(t *testing.T) (*Core, [][]byte, string) { return core, keys, token } -// TestCoreWithTokenStore returns an in-memory core that has a token store -// mounted, so that logical token functions can be used -func TestCoreWithTokenStore(t *testing.T) (*Core, *TokenStore, [][]byte, string) { - c, keys, root := TestCoreUnsealed(t) +func TestCoreUnsealedBackend(t testing.TB, backend physical.Backend) (*Core, [][]byte, string) { + logger := logformat.NewVaultLogger(log.LevelTrace) + conf := testCoreConfig(t, backend, logger) + conf.Seal = &TestSeal{} + core, err := NewCore(conf) + if err != nil { + t.Fatalf("err: %s", err) + } + + keys, token := TestCoreInit(t, core) + for _, key := range keys { + if _, err := TestCoreUnseal(core, TestKeyCopy(key)); err != nil { + t.Fatalf("unseal err: %s", err) + } + } + + sealed, err := core.Sealed() + if err != nil { + t.Fatalf("err checking seal status: %s", err) + } + if sealed { + t.Fatal("should not be sealed") + } + + return core, keys, token +} + +func testTokenStore(t testing.TB, c *Core) *TokenStore { me := &MountEntry{ Table: credentialTableType, Path: "token/", @@ -222,6 +256,25 @@ func TestCoreWithTokenStore(t *testing.T) (*Core, *TokenStore, [][]byte, string) exp := NewExpirationManager(router, subview, ts, logger) ts.SetExpirationManager(exp) + return ts +} + +// TestCoreWithTokenStore returns an in-memory core that has a token store +// mounted, so that logical token functions can be used +func TestCoreWithTokenStore(t testing.TB) (*Core, *TokenStore, [][]byte, string) { + c, keys, root := TestCoreUnsealed(t) + ts := testTokenStore(t, c) + + return c, ts, keys, root +} + +// TestCoreWithBackendTokenStore returns a core that has a token store +// mounted and used the provided physical backend, so that logical token +// functions can be used +func TestCoreWithBackendTokenStore(t testing.TB, backend physical.Backend) (*Core, *TokenStore, [][]byte, string) { + c, keys, root := TestCoreUnsealedBackend(t, backend) + ts := testTokenStore(t, c) + return c, ts, keys, root } @@ -420,7 +473,7 @@ func GenerateRandBytes(length int) ([]byte, error) { return buf, nil } -func TestWaitActive(t *testing.T, core *Core) { +func TestWaitActive(t testing.TB, core *Core) { start := time.Now() var standby bool var err error @@ -463,7 +516,7 @@ func (t *TestClusterCore) CloseListeners() { time.Sleep(time.Second) } -func TestCluster(t *testing.T, handlers []http.Handler, base *CoreConfig, unsealStandbys bool) []*TestClusterCore { +func TestCluster(t testing.TB, handlers []http.Handler, base *CoreConfig, unsealStandbys bool) []*TestClusterCore { if handlers == nil || len(handlers) != 3 { t.Fatal("handlers must be size 3") } From e350a162050e6f1c6db665356468c32683c72117 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 16 Feb 2017 13:37:21 -0500 Subject: [PATCH 10/30] Move ReplicationState to consts --- helper/consts/replication.go | 20 ++++++++++++++++++++ logical/logical.go | 19 ------------------- logical/system_view.go | 12 ++++++++---- vault/core.go | 3 ++- vault/dynamic_system_view.go | 5 +++-- 5 files changed, 33 insertions(+), 26 deletions(-) create mode 100644 helper/consts/replication.go diff --git a/helper/consts/replication.go b/helper/consts/replication.go new file mode 100644 index 0000000000..62bbcb363d --- /dev/null +++ b/helper/consts/replication.go @@ -0,0 +1,20 @@ +package consts + +type ReplicationState uint32 + +const ( + ReplicationDisabled ReplicationState = iota + ReplicationPrimary + ReplicationSecondary +) + +func (r ReplicationState) String() string { + switch r { + case ReplicationSecondary: + return "secondary" + case ReplicationPrimary: + return "primary" + } + + return "disabled" +} diff --git a/logical/logical.go b/logical/logical.go index 9234bfe6d6..3b66fba51f 100644 --- a/logical/logical.go +++ b/logical/logical.go @@ -80,22 +80,3 @@ type Paths struct { // indicates that these paths should not be replicated LocalStorage []string } - -type ReplicationState uint32 - -const ( - ReplicationDisabled ReplicationState = iota - ReplicationPrimary - ReplicationSecondary -) - -func (r ReplicationState) String() string { - switch r { - case ReplicationSecondary: - return "secondary" - case ReplicationPrimary: - return "primary" - } - - return "disabled" -} diff --git a/logical/system_view.go b/logical/system_view.go index 836c865c25..d769397dfc 100644 --- a/logical/system_view.go +++ b/logical/system_view.go @@ -1,6 +1,10 @@ package logical -import "time" +import ( + "time" + + "github.com/hashicorp/vault/helper/consts" +) // SystemView exposes system configuration information in a safe way // for logical backends to consume @@ -32,7 +36,7 @@ type SystemView interface { CachingDisabled() bool // ReplicationState indicates the state of cluster replication - ReplicationState() ReplicationState + ReplicationState() consts.ReplicationState } type StaticSystemView struct { @@ -42,7 +46,7 @@ type StaticSystemView struct { TaintedVal bool CachingDisabledVal bool Primary bool - ReplicationStateVal ReplicationState + ReplicationStateVal consts.ReplicationState } func (d StaticSystemView) DefaultLeaseTTL() time.Duration { @@ -65,6 +69,6 @@ func (d StaticSystemView) CachingDisabled() bool { return d.CachingDisabledVal } -func (d StaticSystemView) ReplicationState() ReplicationState { +func (d StaticSystemView) ReplicationState() consts.ReplicationState { return d.ReplicationStateVal } diff --git a/vault/core.go b/vault/core.go index 4221a90016..f3ffcf5a8c 100644 --- a/vault/core.go +++ b/vault/core.go @@ -23,6 +23,7 @@ import ( "github.com/hashicorp/go-multierror" "github.com/hashicorp/go-uuid" "github.com/hashicorp/vault/audit" + "github.com/hashicorp/vault/helper/consts" "github.com/hashicorp/vault/helper/errutil" "github.com/hashicorp/vault/helper/jsonutil" "github.com/hashicorp/vault/helper/logformat" @@ -310,7 +311,7 @@ type Core struct { // replicationState keeps the current replication state cached for quick // lookup - replicationState logical.ReplicationState + replicationState consts.ReplicationState } // CoreConfig is used to parameterize a core diff --git a/vault/dynamic_system_view.go b/vault/dynamic_system_view.go index e02a061ce3..80b663b951 100644 --- a/vault/dynamic_system_view.go +++ b/vault/dynamic_system_view.go @@ -3,6 +3,7 @@ package vault import ( "time" + "github.com/hashicorp/vault/helper/consts" "github.com/hashicorp/vault/logical" ) @@ -76,8 +77,8 @@ func (d dynamicSystemView) CachingDisabled() bool { } // Checks if this is a primary Vault instance. -func (d dynamicSystemView) ReplicationState() logical.ReplicationState { - var state logical.ReplicationState +func (d dynamicSystemView) ReplicationState() consts.ReplicationState { + var state consts.ReplicationState d.core.clusterParamsLock.RLock() state = d.core.replicationState d.core.clusterParamsLock.RUnlock() From 98c7bd6c03331b460a727e04d54a4fcb0ba723bc Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 16 Feb 2017 15:15:02 -0500 Subject: [PATCH 11/30] Port some replication bits to OSS (#2386) --- .travis.yml | 2 +- Makefile | 5 + README.md | 6 +- .../cassandra/test-fixtures/cassandra.yaml | 8 +- helper/consts/error.go | 13 ++ http/handler.go | 57 +------ http/handler_test.go | 9 +- http/help.go | 2 +- http/logical.go | 33 +--- http/logical_test.go | 2 +- http/sys_audit_test.go | 2 + http/sys_auth_test.go | 8 + http/sys_mount_test.go | 44 ++++++ http/sys_rekey.go | 10 +- http/sys_seal.go | 3 +- logical/connection.go | 2 +- logical/error.go | 24 +++ logical/response_util.go | 111 +++++++++++++ scripts/cross/Dockerfile | 2 +- vault/cluster_test.go | 3 +- vault/core.go | 22 +-- vault/core_test.go | 5 +- vault/generate_root.go | 21 +-- vault/rekey.go | 41 ++--- vault/request_handling.go | 5 +- vault/wrapping.go | 5 +- .../source/docs/internals/replication.html.md | 149 ++++++++++++++++++ website/source/layouts/docs.erb | 4 + 28 files changed, 456 insertions(+), 142 deletions(-) create mode 100644 helper/consts/error.go create mode 100644 logical/response_util.go create mode 100644 website/source/docs/internals/replication.html.md diff --git a/.travis.yml b/.travis.yml index ab3c2d5996..8fd2c16ce1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ services: - docker go: - - 1.8rc2 + - 1.8 matrix: allow_failures: diff --git a/Makefile b/Makefile index f36e9dc48b..732ba93a61 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,11 @@ dev-dynamic: generate test: generate CGO_ENABLED=0 VAULT_TOKEN= VAULT_ACC= go test -tags='$(BUILD_TAGS)' $(TEST) $(TESTARGS) -timeout=10m -parallel=4 +testcompile: generate + @for pkg in $(TEST) ; do \ + go test -v -c -tags='$(BUILD_TAGS)' $$pkg -parallel=4 ; \ + done + # testacc runs acceptance tests testacc: generate @if [ "$(TEST)" = "./..." ]; then \ diff --git a/README.md b/README.md index c7c46fca45..bf40cb75f2 100644 --- a/README.md +++ b/README.md @@ -56,9 +56,9 @@ All documentation is available on the [Vault website](https://www.vaultproject.i Developing Vault -------------------- -If you wish to work on Vault itself or any of its built-in systems, -you'll first need [Go](https://www.golang.org) installed on your -machine (version 1.8+ is *required*). +If you wish to work on Vault itself or any of its built-in systems, you'll +first need [Go](https://www.golang.org) installed on your machine (version 1.8+ +is *required*). For local dev first make sure Go is properly installed, including setting up a [GOPATH](https://golang.org/doc/code.html#GOPATH). Next, clone this repository diff --git a/builtin/logical/cassandra/test-fixtures/cassandra.yaml b/builtin/logical/cassandra/test-fixtures/cassandra.yaml index 54f47d34ac..5b12c8cf4e 100644 --- a/builtin/logical/cassandra/test-fixtures/cassandra.yaml +++ b/builtin/logical/cassandra/test-fixtures/cassandra.yaml @@ -421,7 +421,7 @@ seed_provider: parameters: # seeds is actually a comma-delimited list of addresses. # Ex: ",," - - seeds: "172.17.0.2" + - seeds: "172.17.0.3" # For workloads with more data than can fit in memory, Cassandra's # bottleneck will be reads that need to fetch data from @@ -572,7 +572,7 @@ ssl_storage_port: 7001 # # Setting listen_address to 0.0.0.0 is always wrong. # -listen_address: 172.17.0.2 +listen_address: 172.17.0.3 # Set listen_address OR listen_interface, not both. Interfaces must correspond # to a single address, IP aliasing is not supported. @@ -586,7 +586,7 @@ listen_address: 172.17.0.2 # Address to broadcast to other Cassandra nodes # Leaving this blank will set it to the same value as listen_address -broadcast_address: 172.17.0.2 +broadcast_address: 172.17.0.3 # When using multiple physical network interfaces, set this # to true to listen on broadcast_address in addition to @@ -668,7 +668,7 @@ rpc_port: 9160 # be set to 0.0.0.0. If left blank, this will be set to the value of # rpc_address. If rpc_address is set to 0.0.0.0, broadcast_rpc_address must # be set. -broadcast_rpc_address: 172.17.0.2 +broadcast_rpc_address: 172.17.0.3 # enable or disable keepalive on rpc/native connections rpc_keepalive: true diff --git a/helper/consts/error.go b/helper/consts/error.go new file mode 100644 index 0000000000..d96ba4fe84 --- /dev/null +++ b/helper/consts/error.go @@ -0,0 +1,13 @@ +package consts + +import "errors" + +var ( + // ErrSealed is returned if an operation is performed on a sealed barrier. + // No operation is expected to succeed before unsealing + ErrSealed = errors.New("Vault is sealed") + + // ErrStandby is returned if an operation is performed on a standby Vault. + // No operation is expected to succeed until active. + ErrStandby = errors.New("Vault is in standby mode") +) diff --git a/http/handler.go b/http/handler.go index 8a83af658e..09533b539b 100644 --- a/http/handler.go +++ b/http/handler.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/hashicorp/errwrap" + "github.com/hashicorp/vault/helper/consts" "github.com/hashicorp/vault/helper/duration" "github.com/hashicorp/vault/helper/jsonutil" "github.com/hashicorp/vault/logical" @@ -206,11 +207,11 @@ func handleRequestForwarding(core *vault.Core, handler http.Handler) http.Handle // case of an error. func request(core *vault.Core, w http.ResponseWriter, rawReq *http.Request, r *logical.Request) (*logical.Response, bool) { resp, err := core.HandleRequest(r) - if errwrap.Contains(err, vault.ErrStandby.Error()) { + if errwrap.Contains(err, consts.ErrStandby.Error()) { respondStandby(core, w, rawReq.URL) return resp, false } - if respondErrorCommon(w, resp, err) { + if respondErrorCommon(w, r, resp, err) { return resp, false } @@ -310,20 +311,7 @@ func requestWrapInfo(r *http.Request, req *logical.Request) (*logical.Request, e } func respondError(w http.ResponseWriter, status int, err error) { - // Adjust status code when sealed - if errwrap.Contains(err, vault.ErrSealed.Error()) { - status = http.StatusServiceUnavailable - } - - // Adjust status code on - if errwrap.Contains(err, "http: request body too large") { - status = http.StatusRequestEntityTooLarge - } - - // Allow HTTPCoded error passthrough to specify a code - if t, ok := err.(logical.HTTPCodedError); ok { - status = t.Code() - } + logical.AdjustErrorStatusCode(&status, err) w.Header().Add("Content-Type", "application/json") w.WriteHeader(status) @@ -337,42 +325,13 @@ func respondError(w http.ResponseWriter, status int, err error) { enc.Encode(resp) } -func respondErrorCommon(w http.ResponseWriter, resp *logical.Response, err error) bool { - // If there are no errors return - if err == nil && (resp == nil || !resp.IsError()) { +func respondErrorCommon(w http.ResponseWriter, req *logical.Request, resp *logical.Response, err error) bool { + statusCode, newErr := logical.RespondErrorCommon(req, resp, err) + if newErr == nil && statusCode == 0 { return false } - // Start out with internal server error since in most of these cases there - // won't be a response so this won't be overridden - statusCode := http.StatusInternalServerError - // If we actually have a response, start out with bad request - if resp != nil { - statusCode = http.StatusBadRequest - } - - // Now, check the error itself; if it has a specific logical error, set the - // appropriate code - if err != nil { - switch { - case errwrap.ContainsType(err, new(vault.StatusBadRequest)): - statusCode = http.StatusBadRequest - case errwrap.Contains(err, logical.ErrPermissionDenied.Error()): - statusCode = http.StatusForbidden - case errwrap.Contains(err, logical.ErrUnsupportedOperation.Error()): - statusCode = http.StatusMethodNotAllowed - case errwrap.Contains(err, logical.ErrUnsupportedPath.Error()): - statusCode = http.StatusNotFound - case errwrap.Contains(err, logical.ErrInvalidRequest.Error()): - statusCode = http.StatusBadRequest - } - } - - if resp != nil && resp.IsError() { - err = fmt.Errorf("%s", resp.Data["error"].(string)) - } - - respondError(w, statusCode, err) + respondError(w, statusCode, newErr) return true } diff --git a/http/handler_test.go b/http/handler_test.go index a7b1ad3d03..beab922812 100644 --- a/http/handler_test.go +++ b/http/handler_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/hashicorp/go-cleanhttp" + "github.com/hashicorp/vault/helper/consts" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/vault" ) @@ -80,6 +81,7 @@ func TestSysMounts_headerAuth(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", @@ -88,6 +90,7 @@ func TestSysMounts_headerAuth(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -96,6 +99,7 @@ func TestSysMounts_headerAuth(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, }, "secret/": map[string]interface{}{ @@ -105,6 +109,7 @@ func TestSysMounts_headerAuth(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", @@ -113,6 +118,7 @@ func TestSysMounts_headerAuth(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -121,6 +127,7 @@ func TestSysMounts_headerAuth(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, } testResponseStatus(t, resp, 200) @@ -223,7 +230,7 @@ func TestHandler_error(t *testing.T) { // vault.ErrSealed is a special case w3 := httptest.NewRecorder() - respondError(w3, 400, vault.ErrSealed) + respondError(w3, 400, consts.ErrSealed) if w3.Code != 503 { t.Fatalf("expected 503, got %d", w3.Code) diff --git a/http/help.go b/http/help.go index b7191a929a..f0ca8b1707 100644 --- a/http/help.go +++ b/http/help.go @@ -35,7 +35,7 @@ func handleHelp(core *vault.Core, w http.ResponseWriter, req *http.Request) { resp, err := core.HandleRequest(lreq) if err != nil { - respondErrorCommon(w, resp, err) + respondErrorCommon(w, lreq, resp, err) return } diff --git a/http/logical.go b/http/logical.go index f350bf0c06..88bcb07fc6 100644 --- a/http/logical.go +++ b/http/logical.go @@ -109,40 +109,13 @@ func handleLogical(core *vault.Core, dataOnly bool, prepareRequestCallback Prepa // Make the internal request. We attach the connection info // as well in case this is an authentication request that requires - // it. Vault core handles stripping this if we need to. + // it. Vault core handles stripping this if we need to. This also + // handles all error cases; if we hit respondLogical, the request is a + // success. resp, ok := request(core, w, r, req) if !ok { return } - switch { - case req.Operation == logical.ReadOperation: - if resp == nil { - respondError(w, http.StatusNotFound, nil) - return - } - - // Basically: if we have empty "keys" or no keys at all, 404. This - // provides consistency with GET. - case req.Operation == logical.ListOperation && resp.WrapInfo == nil: - if resp == nil || len(resp.Data) == 0 { - respondError(w, http.StatusNotFound, nil) - return - } - keysRaw, ok := resp.Data["keys"] - if !ok || keysRaw == nil { - respondError(w, http.StatusNotFound, nil) - return - } - keys, ok := keysRaw.([]string) - if !ok { - respondError(w, http.StatusInternalServerError, nil) - return - } - if len(keys) == 0 { - respondError(w, http.StatusNotFound, nil) - return - } - } // Build the proper response respondLogical(w, r, req, dataOnly, resp) diff --git a/http/logical_test.go b/http/logical_test.go index bc4cdfb041..c7b9bb6bd5 100644 --- a/http/logical_test.go +++ b/http/logical_test.go @@ -101,7 +101,7 @@ func TestLogical_StandbyRedirect(t *testing.T) { // Attempt to fix raciness in this test by giving the first core a chance // to grab the lock - time.Sleep(time.Second) + time.Sleep(2 * time.Second) // Create a second HA Vault conf2 := &vault.CoreConfig{ diff --git a/http/sys_audit_test.go b/http/sys_audit_test.go index 499435a77b..58873bfb12 100644 --- a/http/sys_audit_test.go +++ b/http/sys_audit_test.go @@ -35,6 +35,7 @@ func TestSysAudit(t *testing.T) { "type": "noop", "description": "", "options": map[string]interface{}{}, + "local": false, }, }, "noop/": map[string]interface{}{ @@ -42,6 +43,7 @@ func TestSysAudit(t *testing.T) { "type": "noop", "description": "", "options": map[string]interface{}{}, + "local": false, }, } testResponseStatus(t, resp, 200) diff --git a/http/sys_auth_test.go b/http/sys_auth_test.go index 3301e115e6..9e193916f0 100644 --- a/http/sys_auth_test.go +++ b/http/sys_auth_test.go @@ -32,6 +32,7 @@ func TestSysAuth(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, }, "token/": map[string]interface{}{ @@ -41,6 +42,7 @@ func TestSysAuth(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, } testResponseStatus(t, resp, 200) @@ -83,6 +85,7 @@ func TestSysEnableAuth(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "token/": map[string]interface{}{ "description": "token based credentials", @@ -91,6 +94,7 @@ func TestSysEnableAuth(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, }, "foo/": map[string]interface{}{ @@ -100,6 +104,7 @@ func TestSysEnableAuth(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "token/": map[string]interface{}{ "description": "token based credentials", @@ -108,6 +113,7 @@ func TestSysEnableAuth(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, } testResponseStatus(t, resp, 200) @@ -153,6 +159,7 @@ func TestSysDisableAuth(t *testing.T) { }, "description": "token based credentials", "type": "token", + "local": false, }, }, "token/": map[string]interface{}{ @@ -162,6 +169,7 @@ func TestSysDisableAuth(t *testing.T) { }, "description": "token based credentials", "type": "token", + "local": false, }, } testResponseStatus(t, resp, 200) diff --git a/http/sys_mount_test.go b/http/sys_mount_test.go index 513e5b9419..f922299815 100644 --- a/http/sys_mount_test.go +++ b/http/sys_mount_test.go @@ -33,6 +33,7 @@ func TestSysMounts(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", @@ -41,6 +42,7 @@ func TestSysMounts(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -49,6 +51,7 @@ func TestSysMounts(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, }, "secret/": map[string]interface{}{ @@ -58,6 +61,7 @@ func TestSysMounts(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", @@ -66,6 +70,7 @@ func TestSysMounts(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -74,6 +79,7 @@ func TestSysMounts(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, } testResponseStatus(t, resp, 200) @@ -114,6 +120,7 @@ func TestSysMount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "secret/": map[string]interface{}{ "description": "generic secret storage", @@ -122,6 +129,7 @@ func TestSysMount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", @@ -130,6 +138,7 @@ func TestSysMount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -138,6 +147,7 @@ func TestSysMount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, }, "foo/": map[string]interface{}{ @@ -147,6 +157,7 @@ func TestSysMount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "secret/": map[string]interface{}{ "description": "generic secret storage", @@ -155,6 +166,7 @@ func TestSysMount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", @@ -163,6 +175,7 @@ func TestSysMount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -171,6 +184,7 @@ func TestSysMount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, } testResponseStatus(t, resp, 200) @@ -233,6 +247,7 @@ func TestSysRemount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "secret/": map[string]interface{}{ "description": "generic secret storage", @@ -241,6 +256,7 @@ func TestSysRemount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", @@ -249,6 +265,7 @@ func TestSysRemount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -257,6 +274,7 @@ func TestSysRemount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, }, "bar/": map[string]interface{}{ @@ -266,6 +284,7 @@ func TestSysRemount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "secret/": map[string]interface{}{ "description": "generic secret storage", @@ -274,6 +293,7 @@ func TestSysRemount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", @@ -282,6 +302,7 @@ func TestSysRemount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -290,6 +311,7 @@ func TestSysRemount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, } testResponseStatus(t, resp, 200) @@ -333,6 +355,7 @@ func TestSysUnmount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", @@ -341,6 +364,7 @@ func TestSysUnmount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -349,6 +373,7 @@ func TestSysUnmount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, }, "secret/": map[string]interface{}{ @@ -358,6 +383,7 @@ func TestSysUnmount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", @@ -366,6 +392,7 @@ func TestSysUnmount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -374,6 +401,7 @@ func TestSysUnmount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, } testResponseStatus(t, resp, 200) @@ -414,6 +442,7 @@ func TestSysTuneMount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "secret/": map[string]interface{}{ "description": "generic secret storage", @@ -422,6 +451,7 @@ func TestSysTuneMount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", @@ -430,6 +460,7 @@ func TestSysTuneMount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -438,6 +469,7 @@ func TestSysTuneMount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, }, "foo/": map[string]interface{}{ @@ -447,6 +479,7 @@ func TestSysTuneMount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "secret/": map[string]interface{}{ "description": "generic secret storage", @@ -455,6 +488,7 @@ func TestSysTuneMount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", @@ -463,6 +497,7 @@ func TestSysTuneMount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -471,6 +506,7 @@ func TestSysTuneMount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, } testResponseStatus(t, resp, 200) @@ -532,6 +568,7 @@ func TestSysTuneMount(t *testing.T) { "default_lease_ttl": json.Number("259196400"), "max_lease_ttl": json.Number("259200000"), }, + "local": false, }, "secret/": map[string]interface{}{ "description": "generic secret storage", @@ -540,6 +577,7 @@ func TestSysTuneMount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", @@ -548,6 +586,7 @@ func TestSysTuneMount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -556,6 +595,7 @@ func TestSysTuneMount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, }, "foo/": map[string]interface{}{ @@ -565,6 +605,7 @@ func TestSysTuneMount(t *testing.T) { "default_lease_ttl": json.Number("259196400"), "max_lease_ttl": json.Number("259200000"), }, + "local": false, }, "secret/": map[string]interface{}{ "description": "generic secret storage", @@ -573,6 +614,7 @@ func TestSysTuneMount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "sys/": map[string]interface{}{ "description": "system endpoints used for control, policy and debugging", @@ -581,6 +623,7 @@ func TestSysTuneMount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -589,6 +632,7 @@ func TestSysTuneMount(t *testing.T) { "default_lease_ttl": json.Number("0"), "max_lease_ttl": json.Number("0"), }, + "local": false, }, } diff --git a/http/sys_rekey.go b/http/sys_rekey.go index 2d6c970008..023452c185 100644 --- a/http/sys_rekey.go +++ b/http/sys_rekey.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" + "github.com/hashicorp/vault/helper/consts" "github.com/hashicorp/vault/helper/pgpkeys" "github.com/hashicorp/vault/vault" ) @@ -19,6 +20,13 @@ func handleSysRekeyInit(core *vault.Core, recovery bool) http.Handler { return } + repState := core.ReplicationState() + if repState == consts.ReplicationSecondary { + respondError(w, http.StatusBadRequest, + fmt.Errorf("rekeying can only be performed on the primary cluster when replication is activated")) + return + } + switch { case recovery && !core.SealAccess().RecoveryKeySupported(): respondError(w, http.StatusBadRequest, fmt.Errorf("recovery rekeying not supported")) @@ -108,7 +116,7 @@ func handleSysRekeyInitPut(core *vault.Core, recovery bool, w http.ResponseWrite // Right now we don't support this, but the rest of the code is ready for // when we do, hence the check below for this to be false if // StoredShares is greater than zero - if core.SealAccess().StoredKeysSupported() { + if core.SealAccess().StoredKeysSupported() && !recovery { respondError(w, http.StatusBadRequest, fmt.Errorf("rekeying of barrier not supported when stored key support is available")) return } diff --git a/http/sys_seal.go b/http/sys_seal.go index 27e3434ece..07ffbcd5ba 100644 --- a/http/sys_seal.go +++ b/http/sys_seal.go @@ -8,6 +8,7 @@ import ( "net/http" "github.com/hashicorp/errwrap" + "github.com/hashicorp/vault/helper/consts" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/vault" "github.com/hashicorp/vault/version" @@ -126,7 +127,7 @@ func handleSysUnseal(core *vault.Core) http.Handler { case errwrap.Contains(err, vault.ErrBarrierInvalidKey.Error()): case errwrap.Contains(err, vault.ErrBarrierNotInit.Error()): case errwrap.Contains(err, vault.ErrBarrierSealed.Error()): - case errwrap.Contains(err, vault.ErrStandby.Error()): + case errwrap.Contains(err, consts.ErrStandby.Error()): default: respondError(w, http.StatusInternalServerError, err) return diff --git a/logical/connection.go b/logical/connection.go index f14f65567a..d54a0f5327 100644 --- a/logical/connection.go +++ b/logical/connection.go @@ -8,7 +8,7 @@ import ( // is present on the Request structure for credential backends. type Connection struct { // RemoteAddr is the network address that sent the request. - RemoteAddr string + RemoteAddr string `json:"remote_addr"` // ConnState is the TLS connection state if applicable. ConnState *tls.ConnectionState diff --git a/logical/error.go b/logical/error.go index 9d082013f6..19e3e2dea8 100644 --- a/logical/error.go +++ b/logical/error.go @@ -21,3 +21,27 @@ func (e *codedError) Error() string { func (e *codedError) Code() int { return e.code } + +// Struct to identify user input errors. This is helpful in responding the +// appropriate status codes to clients from the HTTP endpoints. +type StatusBadRequest struct { + Err string +} + +// Implementing error interface +func (s *StatusBadRequest) Error() string { + return s.Err +} + +// This is a new type declared to not cause potential compatibility problems if +// the logic around the HTTPCodedError interface changes; in particular for +// logical request paths it is basically ignored, and changing that behavior +// might cause unforseen issues. +type ReplicationCodedError struct { + Msg string + Code int +} + +func (r *ReplicationCodedError) Error() string { + return r.Msg +} diff --git a/logical/response_util.go b/logical/response_util.go new file mode 100644 index 0000000000..a3fd2bfd19 --- /dev/null +++ b/logical/response_util.go @@ -0,0 +1,111 @@ +package logical + +import ( + "errors" + "fmt" + "net/http" + + "github.com/hashicorp/errwrap" + multierror "github.com/hashicorp/go-multierror" + "github.com/hashicorp/vault/helper/consts" +) + +// RespondErrorCommon pulls most of the functionality from http's +// respondErrorCommon and some of http's handleLogical and makes it available +// to both the http package and elsewhere. +func RespondErrorCommon(req *Request, resp *Response, err error) (int, error) { + if err == nil && (resp == nil || !resp.IsError()) { + switch { + case req.Operation == ReadOperation: + if resp == nil { + return http.StatusNotFound, nil + } + + // Basically: if we have empty "keys" or no keys at all, 404. This + // provides consistency with GET. + case req.Operation == ListOperation && resp.WrapInfo == nil: + if resp == nil || len(resp.Data) == 0 { + return http.StatusNotFound, nil + } + keysRaw, ok := resp.Data["keys"] + if !ok || keysRaw == nil { + return http.StatusNotFound, nil + } + keys, ok := keysRaw.([]string) + if !ok { + return http.StatusInternalServerError, nil + } + if len(keys) == 0 { + return http.StatusNotFound, nil + } + } + + return 0, nil + } + + if errwrap.ContainsType(err, new(ReplicationCodedError)) { + var allErrors error + codedErr := errwrap.GetType(err, new(ReplicationCodedError)).(*ReplicationCodedError) + errwrap.Walk(err, func(inErr error) { + newErr, ok := inErr.(*ReplicationCodedError) + if !ok { + allErrors = multierror.Append(allErrors, newErr) + } + }) + if allErrors != nil { + return codedErr.Code, multierror.Append(errors.New(fmt.Sprintf("errors from both primary and secondary; primary error was %v; secondary errors follow", codedErr.Msg)), allErrors) + } + return codedErr.Code, errors.New(codedErr.Msg) + } + + // Start out with internal server error since in most of these cases there + // won't be a response so this won't be overridden + statusCode := http.StatusInternalServerError + // If we actually have a response, start out with bad request + if resp != nil { + statusCode = http.StatusBadRequest + } + + // Now, check the error itself; if it has a specific logical error, set the + // appropriate code + if err != nil { + switch { + case errwrap.ContainsType(err, new(StatusBadRequest)): + statusCode = http.StatusBadRequest + case errwrap.Contains(err, ErrPermissionDenied.Error()): + statusCode = http.StatusForbidden + case errwrap.Contains(err, ErrUnsupportedOperation.Error()): + statusCode = http.StatusMethodNotAllowed + case errwrap.Contains(err, ErrUnsupportedPath.Error()): + statusCode = http.StatusNotFound + case errwrap.Contains(err, ErrInvalidRequest.Error()): + statusCode = http.StatusBadRequest + } + } + + if resp != nil && resp.IsError() { + err = fmt.Errorf("%s", resp.Data["error"].(string)) + } + + return statusCode, err +} + +// AdjustErrorStatusCode adjusts the status that will be sent in error +// conditions in a way that can be shared across http's respondError and other +// locations. +func AdjustErrorStatusCode(status *int, err error) { + // Adjust status code when sealed + if errwrap.Contains(err, consts.ErrSealed.Error()) { + *status = http.StatusServiceUnavailable + } + + // Adjust status code on + if errwrap.Contains(err, "http: request body too large") { + *status = http.StatusRequestEntityTooLarge + } + + // Allow HTTPCoded error passthrough to specify a code + if t, ok := err.(HTTPCodedError); ok { + *status = t.Code() + } +} diff --git a/scripts/cross/Dockerfile b/scripts/cross/Dockerfile index 7d9638b0a2..1194fb02ed 100644 --- a/scripts/cross/Dockerfile +++ b/scripts/cross/Dockerfile @@ -10,7 +10,7 @@ RUN apt-get update -y && apt-get install --no-install-recommends -y -q \ git mercurial bzr \ && rm -rf /var/lib/apt/lists/* -ENV GOVERSION 1.8rc3 +ENV GOVERSION 1.8 RUN mkdir /goroot && mkdir /gopath RUN curl https://storage.googleapis.com/golang/go${GOVERSION}.linux-amd64.tar.gz \ | tar xvzf - -C /goroot --strip-components=1 diff --git a/vault/cluster_test.go b/vault/cluster_test.go index 204d76d467..d3ee5126ff 100644 --- a/vault/cluster_test.go +++ b/vault/cluster_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "github.com/hashicorp/vault/helper/consts" "github.com/hashicorp/vault/helper/logformat" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/physical" @@ -100,7 +101,7 @@ func TestCluster_ListenForRequests(t *testing.T) { checkListenersFunc := func(expectFail bool) { tlsConfig, err := cores[0].ClusterTLSConfig() if err != nil { - if err.Error() != ErrSealed.Error() { + if err.Error() != consts.ErrSealed.Error() { t.Fatal(err) } tlsConfig = lastTLSConfig diff --git a/vault/core.go b/vault/core.go index f3ffcf5a8c..f3b9bf696c 100644 --- a/vault/core.go +++ b/vault/core.go @@ -60,14 +60,6 @@ const ( ) var ( - // ErrSealed is returned if an operation is performed on - // a sealed barrier. No operation is expected to succeed before unsealing - ErrSealed = errors.New("Vault is sealed") - - // ErrStandby is returned if an operation is performed on - // a standby Vault. No operation is expected to succeed until active. - ErrStandby = errors.New("Vault is in standby mode") - // ErrAlreadyInit is returned if the core is already // initialized. This prevents a re-initialization. ErrAlreadyInit = errors.New("Vault is already initialized") @@ -519,10 +511,10 @@ func (c *Core) LookupToken(token string) (*TokenEntry, error) { c.stateLock.RLock() defer c.stateLock.RUnlock() if c.sealed { - return nil, ErrSealed + return nil, consts.ErrSealed } if c.standby { - return nil, ErrStandby + return nil, consts.ErrStandby } // Many tests don't have a token store running @@ -657,7 +649,7 @@ func (c *Core) Leader() (isLeader bool, leaderAddr string, err error) { // Check if sealed if c.sealed { - return false, "", ErrSealed + return false, "", consts.ErrSealed } // Check if HA enabled @@ -1600,6 +1592,14 @@ func (c *Core) emitMetrics(stopCh chan struct{}) { } } +func (c *Core) ReplicationState() consts.ReplicationState { + var state consts.ReplicationState + c.clusterParamsLock.RLock() + state = c.replicationState + c.clusterParamsLock.RUnlock() + return state +} + func (c *Core) SealAccess() *SealAccess { sa := &SealAccess{} sa.SetSeal(c.seal) diff --git a/vault/core_test.go b/vault/core_test.go index 6b9eab04d8..eb991d17b1 100644 --- a/vault/core_test.go +++ b/vault/core_test.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/errwrap" "github.com/hashicorp/go-uuid" "github.com/hashicorp/vault/audit" + "github.com/hashicorp/vault/helper/consts" "github.com/hashicorp/vault/helper/logformat" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/physical" @@ -198,7 +199,7 @@ func TestCore_Route_Sealed(t *testing.T) { Path: "sys/mounts", } _, err := c.HandleRequest(req) - if err != ErrSealed { + if err != consts.ErrSealed { t.Fatalf("err: %v", err) } @@ -1541,7 +1542,7 @@ func testCore_Standby_Common(t *testing.T, inm physical.Backend, inmha physical. // Request should fail in standby mode _, err = core2.HandleRequest(req) - if err != ErrStandby { + if err != consts.ErrStandby { t.Fatalf("err: %v", err) } diff --git a/vault/generate_root.go b/vault/generate_root.go index 0966e96023..4278b022f3 100644 --- a/vault/generate_root.go +++ b/vault/generate_root.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/hashicorp/go-uuid" + "github.com/hashicorp/vault/helper/consts" "github.com/hashicorp/vault/helper/pgpkeys" "github.com/hashicorp/vault/helper/xor" "github.com/hashicorp/vault/shamir" @@ -34,10 +35,10 @@ func (c *Core) GenerateRootProgress() (int, error) { c.stateLock.RLock() defer c.stateLock.RUnlock() if c.sealed { - return 0, ErrSealed + return 0, consts.ErrSealed } if c.standby { - return 0, ErrStandby + return 0, consts.ErrStandby } c.generateRootLock.Lock() @@ -52,10 +53,10 @@ func (c *Core) GenerateRootConfiguration() (*GenerateRootConfig, error) { c.stateLock.RLock() defer c.stateLock.RUnlock() if c.sealed { - return nil, ErrSealed + return nil, consts.ErrSealed } if c.standby { - return nil, ErrStandby + return nil, consts.ErrStandby } c.generateRootLock.Lock() @@ -101,10 +102,10 @@ func (c *Core) GenerateRootInit(otp, pgpKey string) error { c.stateLock.RLock() defer c.stateLock.RUnlock() if c.sealed { - return ErrSealed + return consts.ErrSealed } if c.standby { - return ErrStandby + return consts.ErrStandby } c.generateRootLock.Lock() @@ -170,10 +171,10 @@ func (c *Core) GenerateRootUpdate(key []byte, nonce string) (*GenerateRootResult c.stateLock.RLock() defer c.stateLock.RUnlock() if c.sealed { - return nil, ErrSealed + return nil, consts.ErrSealed } if c.standby { - return nil, ErrStandby + return nil, consts.ErrStandby } c.generateRootLock.Lock() @@ -308,10 +309,10 @@ func (c *Core) GenerateRootCancel() error { c.stateLock.RLock() defer c.stateLock.RUnlock() if c.sealed { - return ErrSealed + return consts.ErrSealed } if c.standby { - return ErrStandby + return consts.ErrStandby } c.generateRootLock.Lock() diff --git a/vault/rekey.go b/vault/rekey.go index 964abef4f0..50f683b6b2 100644 --- a/vault/rekey.go +++ b/vault/rekey.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/hashicorp/go-uuid" + "github.com/hashicorp/vault/helper/consts" "github.com/hashicorp/vault/helper/jsonutil" "github.com/hashicorp/vault/helper/pgpkeys" "github.com/hashicorp/vault/physical" @@ -44,10 +45,10 @@ func (c *Core) RekeyThreshold(recovery bool) (int, error) { c.stateLock.RLock() defer c.stateLock.RUnlock() if c.sealed { - return 0, ErrSealed + return 0, consts.ErrSealed } if c.standby { - return 0, ErrStandby + return 0, consts.ErrStandby } c.rekeyLock.RLock() @@ -72,10 +73,10 @@ func (c *Core) RekeyProgress(recovery bool) (int, error) { c.stateLock.RLock() defer c.stateLock.RUnlock() if c.sealed { - return 0, ErrSealed + return 0, consts.ErrSealed } if c.standby { - return 0, ErrStandby + return 0, consts.ErrStandby } c.rekeyLock.RLock() @@ -92,10 +93,10 @@ func (c *Core) RekeyConfig(recovery bool) (*SealConfig, error) { c.stateLock.RLock() defer c.stateLock.RUnlock() if c.sealed { - return nil, ErrSealed + return nil, consts.ErrSealed } if c.standby { - return nil, ErrStandby + return nil, consts.ErrStandby } c.rekeyLock.Lock() @@ -146,10 +147,10 @@ func (c *Core) BarrierRekeyInit(config *SealConfig) error { c.stateLock.RLock() defer c.stateLock.RUnlock() if c.sealed { - return ErrSealed + return consts.ErrSealed } if c.standby { - return ErrStandby + return consts.ErrStandby } c.rekeyLock.Lock() @@ -196,10 +197,10 @@ func (c *Core) RecoveryRekeyInit(config *SealConfig) error { c.stateLock.RLock() defer c.stateLock.RUnlock() if c.sealed { - return ErrSealed + return consts.ErrSealed } if c.standby { - return ErrStandby + return consts.ErrStandby } c.rekeyLock.Lock() @@ -240,10 +241,10 @@ func (c *Core) BarrierRekeyUpdate(key []byte, nonce string) (*RekeyResult, error c.stateLock.RLock() defer c.stateLock.RUnlock() if c.sealed { - return nil, ErrSealed + return nil, consts.ErrSealed } if c.standby { - return nil, ErrStandby + return nil, consts.ErrStandby } // Verify the key length @@ -422,10 +423,10 @@ func (c *Core) RecoveryRekeyUpdate(key []byte, nonce string) (*RekeyResult, erro c.stateLock.RLock() defer c.stateLock.RUnlock() if c.sealed { - return nil, ErrSealed + return nil, consts.ErrSealed } if c.standby { - return nil, ErrStandby + return nil, consts.ErrStandby } // Verify the key length @@ -589,10 +590,10 @@ func (c *Core) RekeyCancel(recovery bool) error { c.stateLock.RLock() defer c.stateLock.RUnlock() if c.sealed { - return ErrSealed + return consts.ErrSealed } if c.standby { - return ErrStandby + return consts.ErrStandby } c.rekeyLock.Lock() @@ -615,10 +616,10 @@ func (c *Core) RekeyRetrieveBackup(recovery bool) (*RekeyBackup, error) { c.stateLock.RLock() defer c.stateLock.RUnlock() if c.sealed { - return nil, ErrSealed + return nil, consts.ErrSealed } if c.standby { - return nil, ErrStandby + return nil, consts.ErrStandby } c.rekeyLock.RLock() @@ -652,10 +653,10 @@ func (c *Core) RekeyDeleteBackup(recovery bool) error { c.stateLock.RLock() defer c.stateLock.RUnlock() if c.sealed { - return ErrSealed + return consts.ErrSealed } if c.standby { - return ErrStandby + return consts.ErrStandby } c.rekeyLock.Lock() diff --git a/vault/request_handling.go b/vault/request_handling.go index 44b8125507..2c7c454349 100644 --- a/vault/request_handling.go +++ b/vault/request_handling.go @@ -7,6 +7,7 @@ import ( "github.com/armon/go-metrics" "github.com/hashicorp/go-multierror" + "github.com/hashicorp/vault/helper/consts" "github.com/hashicorp/vault/helper/jsonutil" "github.com/hashicorp/vault/helper/policyutil" "github.com/hashicorp/vault/helper/strutil" @@ -18,10 +19,10 @@ func (c *Core) HandleRequest(req *logical.Request) (resp *logical.Response, err c.stateLock.RLock() defer c.stateLock.RUnlock() if c.sealed { - return nil, ErrSealed + return nil, consts.ErrSealed } if c.standby { - return nil, ErrStandby + return nil, consts.ErrStandby } // Allowing writing to a path ending in / makes it extremely difficult to diff --git a/vault/wrapping.go b/vault/wrapping.go index 0dc2e9b59a..46409c39bb 100644 --- a/vault/wrapping.go +++ b/vault/wrapping.go @@ -13,6 +13,7 @@ import ( "github.com/SermoDigital/jose/jws" "github.com/SermoDigital/jose/jwt" "github.com/hashicorp/errwrap" + "github.com/hashicorp/vault/helper/consts" "github.com/hashicorp/vault/helper/jsonutil" "github.com/hashicorp/vault/logical" ) @@ -284,10 +285,10 @@ func (c *Core) ValidateWrappingToken(req *logical.Request) (bool, error) { c.stateLock.RLock() defer c.stateLock.RUnlock() if c.sealed { - return false, ErrSealed + return false, consts.ErrSealed } if c.standby { - return false, ErrStandby + return false, consts.ErrStandby } te, err := c.tokenStore.Lookup(token) diff --git a/website/source/docs/internals/replication.html.md b/website/source/docs/internals/replication.html.md new file mode 100644 index 0000000000..437997436d --- /dev/null +++ b/website/source/docs/internals/replication.html.md @@ -0,0 +1,149 @@ +--- +layout: "docs" +page_title: "Replication" +sidebar_current: "docs-internals-replication" +description: |- + Learn about the details of multi-datacenter replication within Vault. +--- + +# Replication (Vault Enterprise) + +Vault Enterprise 0.7 adds support for multi-datacenter replication. Before +using this feature, it is useful to understand the intended use cases, design +goals, and high level architecture. + +Replication is based on a primary/secondary (1:N) model with asynchronous +replication, focusing on high availability for global deployments. The +trade-offs made in the design and implementation of replication reflect these +high level goals. + +# Use Cases + +Vault replication is based on a number of common use cases: + +* **Multi-Datacenter Deployments**: A common challenge is providing Vault to + applications across many datacenters in a highly-available manner. Running a + single Vault cluster imposes high latency of access for remote clients, + availability loss or outages during connectivity failures, and limits + scalability. + +* **Backup Sites**: Implementing a robust business continuity plan around the + loss of a primary datacenter requires the ability to quickly and easily fail + to a hot backup site. + +* **Scaling Throughput**: Applications that use Vault for + Encryption-as-a-Service or cryptographic offload may generate a very high + volume of requests for Vault. Replicating keys between multiple clusters + allows load to be distributed across additional servers to scale request + throughput. + +# Design Goals + +Based on the use cases for Vault Replication, we had a number of design goals +for the implementation: + +* **Availability**: Global deployments of Vault require high levels of + availability, and can tolerate reduced consistency. During full connectivity, + replication is nearly real-time between the primary and secondary clusters. + Degraded connectivity between a primary and secondary does not impact the + primary's ability to service requests, and the secondary will continue to + service reads on last-known data. + +* **Conflict Free**: Certain replication techniques allow for potential write + conflicts to take place. Particularly, any active/active configuration where + writes are allowed to multiple sites require a conflict resolution strategy. + This varies from techniques that allow for data loss like last-write-wins, or + techniques that require manual operator resolution like allowing multiple + values per key. We avoid the possibility of conflicts to ensure there is no + data loss or manual intervention required. + +* **Transparent to Clients**: Vault replication should be transparent to + clients of Vault, so that existing thin clients work unmodified. The Vault + servers handle the logic of request forwarding to the primary when necessary, + and multi-hop routing is performed internally to ensure requests are + processed. + +* **Simple to Operate**: Operating a replicated cluster should be simple to + avoid administrative overhead and potentially introducing security gaps. + Setup of replication is very simple, and secondaries can handle being + arbitrarily behind the primary, avoiding the need for operator intervention + to copy data or snapshot the primary. + +# Architecture + +The architecture of Vault replication is based on the design goals, focusing on +the intended use cases. When replication is enabled, a cluster is set as either +a _primary_ or _secondary_. The primary cluster is authoritative, and is the +only cluster allowed to perform actions that write to the underlying data +storage, such as modifying policies or secrets. Secondary clusters can service +all other operations, such as reading secrets or sending data through +`transit`, and forward any writes to the primary cluster. Disallowing multiple +primaries ensures the cluster is conflict free and has an authoritative state. + +The primary cluster uses log shipping to replicate changes to all of the +secondaries. This ensures writes are visible globally in near real-time when +there is full network connectivity. If a secondary is down or unable to +communicate with the primary, writes are not blocked on the primary and reads +are still serviced on the secondary. This ensures the availability of Vault. +When the secondary is initialized or recovers from degraded connectivity it +will automatically reconcile with the primary. + +Lastly, clients can speak to any Vault server without a thick client. If a +client is communicating with a standby instance, the request is automatically +forwarded to a active instance. Secondary clusters will service reads locally +and forward any write requests to the primary cluster. The primary cluster is +able to service all request types. + +An important optimization Vault makes is to avoid replication of tokens or +leases between clusters. Policies and secrets are the minority of data managed +by Vault and tend to be relatively stable. Tokens and leases are much more +dynamic, as they are created and expire rapidly. Keeping tokens and leases +locally reduces the amount of data that needs to be replicated, and distributes +the work of TTL management between the clusters. The caveat is that clients +will need to re-authenticate if they switch the Vault cluster they are +communicating with. + +# Implementation Details + +It is important to understand the high-level architecture of replication to +ensure the trade-offs are appropriate for your use case. The implementation +details may be useful for those who are curious or want to understand more +about the performance characteristics or failure scenarios. + +Using replication requires a storage backend that supports transactional +updates, such as Consul. This allows multiple key/value updates to be +performed atomically. Replication uses this to maintain a +[Write-Ahead-Log][wal] (WAL) of all updates, so that the key update happens +atomically with the WAL entry creation. The WALs are then used to perform log +shipping between the Vault clusters. When a secondary is closely synchronized +with a primary, Vault directly streams new WALs to be applied, providing near +real-time replication. A bounded set of WALs are maintained for the +secondaries, and older WALs are garbage collected automatically. + +When a secondary is initialized or is too far behind the primary there may not +be enough WALs to synchronize. To handle this scenario, Vault maintains a +[merkle index][merkle] of the encrypted keys. Any time a key is updated or +deleted, the merkle index is updated to reflect the change. When a secondary +needs to reconcile with a primary, they compare their merkle indexes to +determine which keys are out of sync. The structure of the index allows this to +be done very efficiently, usually requiring only two round trips and a small +amount of data. The secondary uses this information to reconcile and then +switches back into WAL streaming mode. + +Performance is an important concern for Vault, so WAL entries are batched and +the merkle index is not flushed to disk with every operation. Instead, the +index is updated in memory for every operation and asynchronously flushed to +disk. As a result, a crash or power loss may cause the merkle index to become +out of sync with the underlying keys. Vault uses the [ARIES][aries] recovery +algorithm to ensure the consistency of the index under those failure +conditions. + +Log shipping traditionally requires the WAL stream to be synchronized, which +can introduce additional complexity when a new primary cluster is promoted. +Vault uses the merkle index as the source of truth, allowing the WAL streams to +be completely distinct and unsynchronized. This simplifies administration of +Vault Replication for operators. + +[wal]: https://en.wikipedia.org/wiki/Write-ahead_logging +[merkle]: https://en.wikipedia.org/wiki/Merkle_tree +[aries]: https://en.wikipedia.org/wiki/Algorithms_for_Recovery_and_Isolation_Exploiting_Semantics diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 2a06df33ce..ab40d4f6e1 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -32,6 +32,10 @@ > Key Rotation + + > + Replication + From 8acbdefdf2abb71825924e299b5a89e3be14e162 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 16 Feb 2017 16:29:30 -0500 Subject: [PATCH 12/30] More porting from rep (#2388) * More porting from rep * Address review feedback --- builtin/credential/app-id/backend.go | 49 +++++---- builtin/credential/app-id/backend_test.go | 4 + builtin/credential/approle/backend.go | 28 +++-- builtin/credential/approle/backend_test.go | 4 + builtin/credential/aws-ec2/backend.go | 40 +++++-- builtin/credential/cert/backend.go | 15 ++- builtin/credential/cert/path_crls.go | 23 +++- builtin/logical/aws/backend.go | 6 ++ builtin/logical/cassandra/backend.go | 9 ++ builtin/logical/mongodb/backend.go | 9 ++ builtin/logical/mssql/backend.go | 9 ++ builtin/logical/mysql/backend.go | 9 ++ builtin/logical/pki/backend.go | 6 ++ builtin/logical/postgresql/backend.go | 9 ++ builtin/logical/rabbitmq/backend.go | 9 ++ builtin/logical/ssh/backend.go | 27 +++-- builtin/logical/ssh/backend_test.go | 4 + builtin/logical/transit/backend.go | 15 +++ command/mount.go | 7 ++ command/mounts.go | 8 +- command/server.go | 12 ++- command/server/config.go | 9 +- command/server_ha_test.go | 4 +- helper/keysutil/lock_manager.go | 9 ++ vault/logical_cubbyhole.go | 6 ++ vault/logical_cubbyhole_test.go | 10 +- vault/logical_system.go | 117 +++++++++++++++++++++ vault/logical_system_test.go | 7 ++ website/source/layouts/docs.erb | 8 +- 29 files changed, 405 insertions(+), 67 deletions(-) diff --git a/builtin/credential/app-id/backend.go b/builtin/credential/app-id/backend.go index f0bd321487..76d9a6e996 100644 --- a/builtin/credential/app-id/backend.go +++ b/builtin/credential/app-id/backend.go @@ -17,20 +17,10 @@ func Factory(conf *logical.BackendConfig) (logical.Backend, error) { } func Backend(conf *logical.BackendConfig) (*framework.Backend, error) { - // Initialize the salt - salt, err := salt.NewSalt(conf.StorageView, &salt.Config{ - HashFunc: salt.SHA1Hash, - }) - if err != nil { - return nil, err - } - var b backend - b.Salt = salt b.MapAppId = &framework.PolicyMap{ PathMap: framework.PathMap{ Name: "app-id", - Salt: salt, Schema: map[string]*framework.FieldSchema{ "display_name": &framework.FieldSchema{ Type: framework.TypeString, @@ -48,7 +38,6 @@ func Backend(conf *logical.BackendConfig) (*framework.Backend, error) { b.MapUserId = &framework.PathMap{ Name: "user-id", - Salt: salt, Schema: map[string]*framework.FieldSchema{ "cidr_block": &framework.FieldSchema{ Type: framework.TypeString, @@ -81,17 +70,11 @@ func Backend(conf *logical.BackendConfig) (*framework.Backend, error) { ), AuthRenew: b.pathLoginRenew, + + Init: b.initialize, } - // Since the salt is new in 0.2, we need to handle this by migrating - // any existing keys to use the salt. We can deprecate this eventually, - // but for now we want a smooth upgrade experience by automatically - // upgrading to use salting. - if salt.DidGenerate() { - if err := b.upgradeToSalted(conf.StorageView); err != nil { - return nil, err - } - } + b.view = conf.StorageView return b.Backend, nil } @@ -100,10 +83,36 @@ type backend struct { *framework.Backend Salt *salt.Salt + view logical.Storage MapAppId *framework.PolicyMap MapUserId *framework.PathMap } +func (b *backend) initialize() error { + salt, err := salt.NewSalt(b.view, &salt.Config{ + HashFunc: salt.SHA1Hash, + }) + if err != nil { + return err + } + b.Salt = salt + + b.MapAppId.Salt = salt + b.MapUserId.Salt = salt + + // Since the salt is new in 0.2, we need to handle this by migrating + // any existing keys to use the salt. We can deprecate this eventually, + // but for now we want a smooth upgrade experience by automatically + // upgrading to use salting. + if salt.DidGenerate() { + if err := b.upgradeToSalted(b.view); err != nil { + return err + } + } + + return nil +} + // upgradeToSalted is used to upgrade the non-salted keys prior to // Vault 0.2 to be salted. This is done on mount time and is only // done once. It can be deprecated eventually, but should be around diff --git a/builtin/credential/app-id/backend_test.go b/builtin/credential/app-id/backend_test.go index 55bb3e9d85..2960e4060e 100644 --- a/builtin/credential/app-id/backend_test.go +++ b/builtin/credential/app-id/backend_test.go @@ -72,6 +72,10 @@ func TestBackend_upgradeToSalted(t *testing.T) { if err != nil { t.Fatalf("err: %v", err) } + err = backend.Initialize() + if err != nil { + t.Fatalf("err: %v", err) + } // Check the keys have been upgraded out, err := inm.Get("struct/map/app-id/foo") diff --git a/builtin/credential/approle/backend.go b/builtin/credential/approle/backend.go index df10e9d1bb..d40b75ebff 100644 --- a/builtin/credential/approle/backend.go +++ b/builtin/credential/approle/backend.go @@ -17,6 +17,9 @@ type backend struct { // by this backend. salt *salt.Salt + // The view to use when creating the salt + view logical.Storage + // Guard to clean-up the expired SecretID entries tidySecretIDCASGuard uint32 @@ -57,18 +60,9 @@ func Factory(conf *logical.BackendConfig) (logical.Backend, error) { } func Backend(conf *logical.BackendConfig) (*backend, error) { - // Initialize the salt - salt, err := salt.NewSalt(conf.StorageView, &salt.Config{ - HashFunc: salt.SHA256Hash, - }) - if err != nil { - return nil, err - } - // Create a backend object b := &backend{ - // Set the salt object for the backend - salt: salt, + view: conf.StorageView, // Create the map of locks to modify the registered roles roleLocksMap: make(map[string]*sync.RWMutex, 257), @@ -83,6 +77,8 @@ func Backend(conf *logical.BackendConfig) (*backend, error) { secretIDAccessorLocksMap: make(map[string]*sync.RWMutex, 257), } + var err error + // Create 256 locks each for managing RoleID and SecretIDs. This will avoid // a superfluous number of locks directly proportional to the number of RoleID // and SecretIDs. These locks can be accessed by indexing based on the first two @@ -129,10 +125,22 @@ func Backend(conf *logical.BackendConfig) (*backend, error) { pathTidySecretID(b), }, ), + Init: b.initialize, } return b, nil } +func (b *backend) initialize() error { + salt, err := salt.NewSalt(b.view, &salt.Config{ + HashFunc: salt.SHA256Hash, + }) + if err != nil { + return err + } + b.salt = salt + return nil +} + // periodicFunc of the backend will be invoked once a minute by the RollbackManager. // RoleRole backend utilizes this function to delete expired SecretID entries. // This could mean that the SecretID may live in the backend upto 1 min after its diff --git a/builtin/credential/approle/backend_test.go b/builtin/credential/approle/backend_test.go index 2a3e3773ec..e49cf48f8b 100644 --- a/builtin/credential/approle/backend_test.go +++ b/builtin/credential/approle/backend_test.go @@ -21,5 +21,9 @@ func createBackendWithStorage(t *testing.T) (*backend, logical.Storage) { if err != nil { t.Fatal(err) } + err = b.Initialize() + if err != nil { + t.Fatal(err) + } return b, config.StorageView } diff --git a/builtin/credential/aws-ec2/backend.go b/builtin/credential/aws-ec2/backend.go index f24f3bfe88..33ef4a80ce 100644 --- a/builtin/credential/aws-ec2/backend.go +++ b/builtin/credential/aws-ec2/backend.go @@ -23,6 +23,9 @@ type backend struct { *framework.Backend Salt *salt.Salt + // Used during initialization to set the salt + view logical.Storage + // Lock to make changes to any of the backend's configuration endpoints. configMutex sync.RWMutex @@ -59,18 +62,11 @@ type backend struct { } func Backend(conf *logical.BackendConfig) (*backend, error) { - salt, err := salt.NewSalt(conf.StorageView, &salt.Config{ - HashFunc: salt.SHA256Hash, - }) - if err != nil { - return nil, err - } - b := &backend{ // Setting the periodic func to be run once in an hour. // If there is a real need, this can be made configurable. tidyCooldownPeriod: time.Hour, - Salt: salt, + view: conf.StorageView, EC2ClientsMap: make(map[string]map[string]*ec2.EC2), IAMClientsMap: make(map[string]map[string]*iam.IAM), } @@ -83,6 +79,9 @@ func Backend(conf *logical.BackendConfig) (*backend, error) { Unauthenticated: []string{ "login", }, + LocalStorage: []string{ + "whitelist/identity/", + }, }, Paths: []*framework.Path{ pathLogin(b), @@ -104,11 +103,26 @@ func Backend(conf *logical.BackendConfig) (*backend, error) { pathIdentityWhitelist(b), pathTidyIdentityWhitelist(b), }, + + Invalidate: b.invalidate, + + Init: b.initialize, } return b, nil } +func (b *backend) initialize() error { + salt, err := salt.NewSalt(b.view, &salt.Config{ + HashFunc: salt.SHA256Hash, + }) + if err != nil { + return err + } + b.Salt = salt + return nil +} + // periodicFunc performs the tasks that the backend wishes to do periodically. // Currently this will be triggered once in a minute by the RollbackManager. // @@ -169,6 +183,16 @@ func (b *backend) periodicFunc(req *logical.Request) error { return nil } +func (b *backend) invalidate(key string) { + switch key { + case "config/client": + b.configMutex.Lock() + defer b.configMutex.Unlock() + b.flushCachedEC2Clients() + b.flushCachedIAMClients() + } +} + const backendHelp = ` aws-ec2 auth backend takes in PKCS#7 signature of an AWS EC2 instance and a client created nonce to authenticates the EC2 instance with Vault. diff --git a/builtin/credential/cert/backend.go b/builtin/credential/cert/backend.go index a07ec33671..088cc41ab1 100644 --- a/builtin/credential/cert/backend.go +++ b/builtin/credential/cert/backend.go @@ -1,6 +1,7 @@ package cert import ( + "strings" "sync" "github.com/hashicorp/vault/logical" @@ -13,7 +14,7 @@ func Factory(conf *logical.BackendConfig) (logical.Backend, error) { if err != nil { return b, err } - return b, b.populateCRLs(conf.StorageView) + return b, nil } func Backend() *backend { @@ -36,9 +37,10 @@ func Backend() *backend { }), AuthRenew: b.pathLoginRenew, + + Invalidate: b.invalidate, } - b.crls = map[string]CRLInfo{} b.crlUpdateMutex = &sync.RWMutex{} return &b @@ -52,6 +54,15 @@ type backend struct { crlUpdateMutex *sync.RWMutex } +func (b *backend) invalidate(key string) { + switch { + case strings.HasPrefix(key, "crls/"): + b.crlUpdateMutex.Lock() + defer b.crlUpdateMutex.Unlock() + b.crls = nil + } +} + const backendHelp = ` The "cert" credential provider allows authentication using TLS client certificates. A client connects to Vault and uses diff --git a/builtin/credential/cert/path_crls.go b/builtin/credential/cert/path_crls.go index f45effbdd4..234b93adc7 100644 --- a/builtin/credential/cert/path_crls.go +++ b/builtin/credential/cert/path_crls.go @@ -45,6 +45,12 @@ func (b *backend) populateCRLs(storage logical.Storage) error { b.crlUpdateMutex.Lock() defer b.crlUpdateMutex.Unlock() + if b.crls != nil { + return nil + } + + b.crls = map[string]CRLInfo{} + keys, err := storage.List("crls/") if err != nil { return fmt.Errorf("error listing CRLs: %v", err) @@ -56,6 +62,7 @@ func (b *backend) populateCRLs(storage logical.Storage) error { for _, key := range keys { entry, err := storage.Get("crls/" + key) if err != nil { + b.crls = nil return fmt.Errorf("error loading CRL %s: %v", key, err) } if entry == nil { @@ -64,6 +71,7 @@ func (b *backend) populateCRLs(storage logical.Storage) error { var crlInfo CRLInfo err = entry.DecodeJSON(&crlInfo) if err != nil { + b.crls = nil return fmt.Errorf("error decoding CRL %s: %v", key, err) } b.crls[key] = crlInfo @@ -121,6 +129,10 @@ func (b *backend) pathCRLDelete( return logical.ErrorResponse(`"name" parameter cannot be empty`), nil } + if err := b.populateCRLs(req.Storage); err != nil { + return nil, err + } + b.crlUpdateMutex.Lock() defer b.crlUpdateMutex.Unlock() @@ -131,8 +143,7 @@ func (b *backend) pathCRLDelete( )), nil } - err := req.Storage.Delete("crls/" + name) - if err != nil { + if err := req.Storage.Delete("crls/" + name); err != nil { return logical.ErrorResponse(fmt.Sprintf( "error deleting crl %s: %v", name, err), ), nil @@ -150,6 +161,10 @@ func (b *backend) pathCRLRead( return logical.ErrorResponse(`"name" parameter must be set`), nil } + if err := b.populateCRLs(req.Storage); err != nil { + return nil, err + } + b.crlUpdateMutex.RLock() defer b.crlUpdateMutex.RUnlock() @@ -185,6 +200,10 @@ func (b *backend) pathCRLWrite( return logical.ErrorResponse("parsed CRL is nil"), nil } + if err := b.populateCRLs(req.Storage); err != nil { + return nil, err + } + b.crlUpdateMutex.Lock() defer b.crlUpdateMutex.Unlock() diff --git a/builtin/logical/aws/backend.go b/builtin/logical/aws/backend.go index 6c9ced381b..246e25cf24 100644 --- a/builtin/logical/aws/backend.go +++ b/builtin/logical/aws/backend.go @@ -17,6 +17,12 @@ func Backend() *backend { b.Backend = &framework.Backend{ Help: strings.TrimSpace(backendHelp), + PathsSpecial: &logical.Paths{ + LocalStorage: []string{ + framework.WALPrefix, + }, + }, + Paths: []*framework.Path{ pathConfigRoot(), pathConfigLease(&b), diff --git a/builtin/logical/cassandra/backend.go b/builtin/logical/cassandra/backend.go index 81149f806d..c2e769ccb4 100644 --- a/builtin/logical/cassandra/backend.go +++ b/builtin/logical/cassandra/backend.go @@ -31,6 +31,8 @@ func Backend() *backend { secretCreds(&b), }, + Invalidate: b.invalidate, + Clean: func() { b.ResetDB(nil) }, @@ -107,6 +109,13 @@ func (b *backend) ResetDB(newSession *gocql.Session) { b.session = newSession } +func (b *backend) invalidate(key string) { + switch key { + case "config/connection": + b.ResetDB(nil) + } +} + const backendHelp = ` The Cassandra backend dynamically generates database users. diff --git a/builtin/logical/mongodb/backend.go b/builtin/logical/mongodb/backend.go index 8360e971da..e9f3b29add 100644 --- a/builtin/logical/mongodb/backend.go +++ b/builtin/logical/mongodb/backend.go @@ -33,6 +33,8 @@ func Backend() *framework.Backend { }, Clean: b.ResetSession, + + Invalidate: b.invalidate, } return b.Backend @@ -97,6 +99,13 @@ func (b *backend) ResetSession() { b.session = nil } +func (b *backend) invalidate(key string) { + switch key { + case "config/connection": + b.ResetSession() + } +} + // LeaseConfig returns the lease configuration func (b *backend) LeaseConfig(s logical.Storage) (*configLease, error) { entry, err := s.Get("config/lease") diff --git a/builtin/logical/mssql/backend.go b/builtin/logical/mssql/backend.go index 02597788b5..2a7689f17f 100644 --- a/builtin/logical/mssql/backend.go +++ b/builtin/logical/mssql/backend.go @@ -32,6 +32,8 @@ func Backend() *backend { secretCreds(&b), }, + Invalidate: b.invalidate, + Clean: b.ResetDB, } @@ -112,6 +114,13 @@ func (b *backend) ResetDB() { b.db = nil } +func (b *backend) invalidate(key string) { + switch key { + case "config/connection": + b.ResetDB() + } +} + // LeaseConfig returns the lease configuration func (b *backend) LeaseConfig(s logical.Storage) (*configLease, error) { entry, err := s.Get("config/lease") diff --git a/builtin/logical/mysql/backend.go b/builtin/logical/mysql/backend.go index 6bc3057340..7ae0335e1a 100644 --- a/builtin/logical/mysql/backend.go +++ b/builtin/logical/mysql/backend.go @@ -32,6 +32,8 @@ func Backend() *backend { secretCreds(&b), }, + Invalidate: b.invalidate, + Clean: b.ResetDB, } @@ -105,6 +107,13 @@ func (b *backend) ResetDB() { b.db = nil } +func (b *backend) invalidate(key string) { + switch key { + case "config/connection": + b.ResetDB() + } +} + // Lease returns the lease information func (b *backend) Lease(s logical.Storage) (*configLease, error) { entry, err := s.Get("config/lease") diff --git a/builtin/logical/pki/backend.go b/builtin/logical/pki/backend.go index 19e2cbc075..6128028346 100644 --- a/builtin/logical/pki/backend.go +++ b/builtin/logical/pki/backend.go @@ -29,6 +29,12 @@ func Backend() *backend { "crl/pem", "crl", }, + + LocalStorage: []string{ + "revoked/", + "crl", + "certs/", + }, }, Paths: []*framework.Path{ diff --git a/builtin/logical/postgresql/backend.go b/builtin/logical/postgresql/backend.go index 8dc59aa211..6f4befd8a1 100644 --- a/builtin/logical/postgresql/backend.go +++ b/builtin/logical/postgresql/backend.go @@ -34,6 +34,8 @@ func Backend(conf *logical.BackendConfig) *backend { }, Clean: b.ResetDB, + + Invalidate: b.invalidate, } b.logger = conf.Logger @@ -126,6 +128,13 @@ func (b *backend) ResetDB() { b.db = nil } +func (b *backend) invalidate(key string) { + switch key { + case "config/connection": + b.ResetDB() + } +} + // Lease returns the lease information func (b *backend) Lease(s logical.Storage) (*configLease, error) { entry, err := s.Get("config/lease") diff --git a/builtin/logical/rabbitmq/backend.go b/builtin/logical/rabbitmq/backend.go index e0123753e5..4f9cde0d7e 100644 --- a/builtin/logical/rabbitmq/backend.go +++ b/builtin/logical/rabbitmq/backend.go @@ -35,6 +35,8 @@ func Backend() *backend { }, Clean: b.resetClient, + + Invalidate: b.invalidate, } return &b @@ -99,6 +101,13 @@ func (b *backend) resetClient() { b.client = nil } +func (b *backend) invalidate(key string) { + switch key { + case "config/connection": + b.resetClient() + } +} + // Lease returns the lease information func (b *backend) Lease(s logical.Storage) (*configLease, error) { entry, err := s.Get("config/lease") diff --git a/builtin/logical/ssh/backend.go b/builtin/logical/ssh/backend.go index d39cae2a50..06fc4ccd82 100644 --- a/builtin/logical/ssh/backend.go +++ b/builtin/logical/ssh/backend.go @@ -10,6 +10,7 @@ import ( type backend struct { *framework.Backend + view logical.Storage salt *salt.Salt } @@ -22,15 +23,8 @@ func Factory(conf *logical.BackendConfig) (logical.Backend, error) { } func Backend(conf *logical.BackendConfig) (*backend, error) { - salt, err := salt.NewSalt(conf.StorageView, &salt.Config{ - HashFunc: salt.SHA256Hash, - }) - if err != nil { - return nil, err - } - var b backend - b.salt = salt + b.view = conf.StorageView b.Backend = &framework.Backend{ Help: strings.TrimSpace(backendHelp), @@ -38,6 +32,10 @@ func Backend(conf *logical.BackendConfig) (*backend, error) { Unauthenticated: []string{ "verify", }, + + LocalStorage: []string{ + "otp/", + }, }, Paths: []*framework.Path{ @@ -54,10 +52,23 @@ func Backend(conf *logical.BackendConfig) (*backend, error) { secretDynamicKey(&b), secretOTP(&b), }, + + Init: b.Initialize, } return &b, nil } +func (b *backend) Initialize() error { + salt, err := salt.NewSalt(b.view, &salt.Config{ + HashFunc: salt.SHA256Hash, + }) + if err != nil { + return err + } + b.salt = salt + return nil +} + const backendHelp = ` The SSH backend generates credentials allowing clients to establish SSH connections to remote hosts. diff --git a/builtin/logical/ssh/backend_test.go b/builtin/logical/ssh/backend_test.go index 2fa3a0be21..5012465326 100644 --- a/builtin/logical/ssh/backend_test.go +++ b/builtin/logical/ssh/backend_test.go @@ -73,6 +73,10 @@ func TestBackend_allowed_users(t *testing.T) { if err != nil { t.Fatal(err) } + err = b.Initialize() + if err != nil { + t.Fatal(err) + } roleData := map[string]interface{}{ "key_type": "otp", diff --git a/builtin/logical/transit/backend.go b/builtin/logical/transit/backend.go index 7737a8a756..37ebca43e4 100644 --- a/builtin/logical/transit/backend.go +++ b/builtin/logical/transit/backend.go @@ -1,6 +1,8 @@ package transit import ( + "strings" + "github.com/hashicorp/vault/helper/keysutil" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" @@ -39,6 +41,8 @@ func Backend(conf *logical.BackendConfig) *backend { }, Secrets: []*framework.Secret{}, + + Invalidate: b.invalidate, } b.lm = keysutil.NewLockManager(conf.System.CachingDisabled()) @@ -50,3 +54,14 @@ type backend struct { *framework.Backend lm *keysutil.LockManager } + +func (b *backend) invalidate(key string) { + if b.Logger().IsTrace() { + b.Logger().Trace("transit: invalidating key", "key", key) + } + switch { + case strings.HasPrefix(key, "policy/"): + name := strings.TrimPrefix(key, "policy/") + b.lm.InvalidatePolicy(name) + } +} diff --git a/command/mount.go b/command/mount.go index bfdba87591..9700dccdbf 100644 --- a/command/mount.go +++ b/command/mount.go @@ -15,11 +15,13 @@ type MountCommand struct { func (c *MountCommand) Run(args []string) int { var description, path, defaultLeaseTTL, maxLeaseTTL string + var local bool flags := c.Meta.FlagSet("mount", meta.FlagSetDefault) flags.StringVar(&description, "description", "", "") flags.StringVar(&path, "path", "", "") flags.StringVar(&defaultLeaseTTL, "default-lease-ttl", "", "") flags.StringVar(&maxLeaseTTL, "max-lease-ttl", "", "") + flags.BoolVar(&local, "local", false, "") flags.Usage = func() { c.Ui.Error(c.Help()) } if err := flags.Parse(args); err != nil { return 1 @@ -54,6 +56,7 @@ func (c *MountCommand) Run(args []string) int { DefaultLeaseTTL: defaultLeaseTTL, MaxLeaseTTL: maxLeaseTTL, }, + Local: local, } if err := client.Sys().Mount(path, mountInfo); err != nil { @@ -102,6 +105,10 @@ Mount Options: the previously set value. Set to '0' to explicitly set it to use the global default. + -local Mark the mount as a local mount. Local mounts + are not replicated nor (if a secondary) + removed by replication. + ` return strings.TrimSpace(helpText) } diff --git a/command/mounts.go b/command/mounts.go index 823aa57e75..d57837dd2c 100644 --- a/command/mounts.go +++ b/command/mounts.go @@ -42,7 +42,7 @@ func (c *MountsCommand) Run(args []string) int { } sort.Strings(paths) - columns := []string{"Path | Type | Default TTL | Max TTL | Description"} + columns := []string{"Path | Type | Default TTL | Max TTL | Replication Behavior | Description"} for _, path := range paths { mount := mounts[path] defTTL := "system" @@ -63,8 +63,12 @@ func (c *MountsCommand) Run(args []string) int { case mount.Config.MaxLeaseTTL != 0: maxTTL = strconv.Itoa(mount.Config.MaxLeaseTTL) } + replicatedBehavior := "replicated" + if mount.Local { + replicatedBehavior = "local" + } columns = append(columns, fmt.Sprintf( - "%s | %s | %s | %s | %s", path, mount.Type, defTTL, maxTTL, mount.Description)) + "%s | %s | %s | %s | %s | %s", path, mount.Type, defTTL, maxTTL, replicatedBehavior, mount.Description)) } c.Ui.Output(columnize.SimpleFormat(columns)) diff --git a/command/server.go b/command/server.go index cf8a442d49..e0b81f90a4 100644 --- a/command/server.go +++ b/command/server.go @@ -61,7 +61,7 @@ type ServerCommand struct { } func (c *ServerCommand) Run(args []string) int { - var dev, verifyOnly, devHA bool + var dev, verifyOnly, devHA, devTransactional bool var configPath []string var logLevel, devRootTokenID, devListenAddress string flags := c.Meta.FlagSet("server", meta.FlagSetDefault) @@ -70,7 +70,8 @@ func (c *ServerCommand) Run(args []string) int { flags.StringVar(&devListenAddress, "dev-listen-address", "", "") flags.StringVar(&logLevel, "log-level", "info", "") flags.BoolVar(&verifyOnly, "verify-only", false, "") - flags.BoolVar(&devHA, "dev-ha", false, "") + flags.BoolVar(&devHA, "ha", false, "") + flags.BoolVar(&devTransactional, "transactional", false, "") flags.Usage = func() { c.Ui.Output(c.Help()) } flags.Var((*sliceflag.StringFlag)(&configPath), "config", "config") if err := flags.Parse(args); err != nil { @@ -122,7 +123,7 @@ func (c *ServerCommand) Run(args []string) int { devListenAddress = os.Getenv("VAULT_DEV_LISTEN_ADDRESS") } - if devHA { + if devHA || devTransactional { dev = true } @@ -143,7 +144,7 @@ func (c *ServerCommand) Run(args []string) int { // Load the configuration var config *server.Config if dev { - config = server.DevConfig(devHA) + config = server.DevConfig(devHA, devTransactional) if devListenAddress != "" { config.Listeners[0].Config["address"] = devListenAddress } @@ -235,6 +236,9 @@ func (c *ServerCommand) Run(args []string) int { ClusterName: config.ClusterName, CacheSize: config.CacheSize, } + if dev { + coreConfig.DevToken = devRootTokenID + } var disableClustering bool diff --git a/command/server/config.go b/command/server/config.go index 8840aa5efc..77bece5b7f 100644 --- a/command/server/config.go +++ b/command/server/config.go @@ -38,7 +38,7 @@ type Config struct { } // DevConfig is a Config that is used for dev mode of Vault. -func DevConfig(ha bool) *Config { +func DevConfig(ha, transactional bool) *Config { ret := &Config{ DisableCache: false, DisableMlock: true, @@ -63,7 +63,12 @@ func DevConfig(ha bool) *Config { DefaultLeaseTTL: 32 * 24 * time.Hour, } - if ha { + switch { + case ha && transactional: + ret.Backend.Type = "inmem_transactional_ha" + case !ha && transactional: + ret.Backend.Type = "inmem_transactional" + case ha && !transactional: ret.Backend.Type = "inmem_ha" } diff --git a/command/server_ha_test.go b/command/server_ha_test.go index 30280c5571..26dc00878f 100644 --- a/command/server_ha_test.go +++ b/command/server_ha_test.go @@ -33,7 +33,7 @@ func TestServer_CommonHA(t *testing.T) { args := []string{"-config", tmpfile.Name(), "-verify-only", "true"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + t.Fatalf("bad: %d\n\n%s\n\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) } if !strings.Contains(ui.OutputWriter.String(), "(HA available)") { @@ -61,7 +61,7 @@ func TestServer_GoodSeparateHA(t *testing.T) { args := []string{"-config", tmpfile.Name(), "-verify-only", "true"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + t.Fatalf("bad: %d\n\n%s\n\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) } if !strings.Contains(ui.OutputWriter.String(), "HA Backend:") { diff --git a/helper/keysutil/lock_manager.go b/helper/keysutil/lock_manager.go index 1549c27401..380c39af11 100644 --- a/helper/keysutil/lock_manager.go +++ b/helper/keysutil/lock_manager.go @@ -71,6 +71,15 @@ func (lm *LockManager) CacheActive() bool { return lm.cache != nil } +func (lm *LockManager) InvalidatePolicy(name string) { + // Check if it's in our cache. If so, return right away. + if lm.CacheActive() { + lm.cacheMutex.Lock() + defer lm.cacheMutex.Unlock() + delete(lm.cache, name) + } +} + func (lm *LockManager) policyLock(name string, lockType bool) *sync.RWMutex { lm.locksMutex.RLock() lock := lm.locks[name] diff --git a/vault/logical_cubbyhole.go b/vault/logical_cubbyhole.go index 76353b0bed..697655538f 100644 --- a/vault/logical_cubbyhole.go +++ b/vault/logical_cubbyhole.go @@ -16,6 +16,12 @@ func CubbyholeBackendFactory(conf *logical.BackendConfig) (logical.Backend, erro b.Backend = &framework.Backend{ Help: strings.TrimSpace(cubbyholeHelp), + PathsSpecial: &logical.Paths{ + LocalStorage: []string{ + "*", + }, + }, + Paths: []*framework.Path{ &framework.Path{ Pattern: ".*", diff --git a/vault/logical_cubbyhole_test.go b/vault/logical_cubbyhole_test.go index fcf3ec3a0c..79531d6e39 100644 --- a/vault/logical_cubbyhole_test.go +++ b/vault/logical_cubbyhole_test.go @@ -12,9 +12,13 @@ import ( func TestCubbyholeBackend_RootPaths(t *testing.T) { b := testCubbyholeBackend() - root := b.SpecialPaths() - if root != nil { - t.Fatalf("unexpected: %v", root) + expected := []string{ + "*", + } + + actual := b.SpecialPaths().LocalStorage + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %#v", actual) } } diff --git a/vault/logical_system.go b/vault/logical_system.go index 7438003218..e0f1163214 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -9,6 +9,7 @@ import ( "sync" "time" + "github.com/hashicorp/vault/helper/consts" "github.com/hashicorp/vault/helper/duration" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" @@ -39,12 +40,14 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen "audit", "audit/*", "raw/*", + "replication/primary/secondary-token", "rotate", "config/auditing/*", }, Unauthenticated: []string{ "wrapping/pubkey", + "replication/status", }, }, @@ -226,6 +229,11 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen Type: framework.TypeMap, Description: strings.TrimSpace(sysHelp["mount_config"][0]), }, + "local": &framework.FieldSchema{ + Type: framework.TypeBool, + Default: false, + Description: strings.TrimSpace(sysHelp["mount_local"][0]), + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -377,6 +385,11 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen Type: framework.TypeString, Description: strings.TrimSpace(sysHelp["auth_desc"][0]), }, + "local": &framework.FieldSchema{ + Type: framework.TypeBool, + Default: false, + Description: strings.TrimSpace(sysHelp["mount_local"][0]), + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -495,6 +508,11 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen Type: framework.TypeMap, Description: strings.TrimSpace(sysHelp["audit_opts"][0]), }, + "local": &framework.FieldSchema{ + Type: framework.TypeBool, + Default: false, + Description: strings.TrimSpace(sysHelp["mount_local"][0]), + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -657,6 +675,10 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) (logical.Backen }, } + b.Backend.Paths = append(b.Backend.Paths, b.replicationPaths()...) + + b.Backend.Invalidate = b.invalidate + return b.Backend.Setup(config) } @@ -668,6 +690,20 @@ type SystemBackend struct { Backend *framework.Backend } +func (b *SystemBackend) invalidate(key string) { + if b.Core.logger.IsTrace() { + b.Core.logger.Trace("sys: invaliding key", "key", key) + } + switch { + case strings.HasPrefix(key, policySubPath): + b.Core.stateLock.RLock() + defer b.Core.stateLock.RUnlock() + if b.Core.policyStore != nil { + b.Core.policyStore.invalidate(strings.TrimPrefix(key, policySubPath)) + } + } +} + // handleAuditedHeaderUpdate creates or overwrites a header entry func (b *SystemBackend) handleAuditedHeaderUpdate(req *logical.Request, d *framework.FieldData) (*logical.Response, error) { header := d.Get("header").(string) @@ -869,6 +905,7 @@ func (b *SystemBackend) handleMountTable( "default_lease_ttl": int64(entry.Config.DefaultLeaseTTL.Seconds()), "max_lease_ttl": int64(entry.Config.MaxLeaseTTL.Seconds()), }, + "local": entry.Local, } resp.Data[entry.Path] = info @@ -880,6 +917,15 @@ func (b *SystemBackend) handleMountTable( // handleMount is used to mount a new path func (b *SystemBackend) handleMount( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + b.Core.clusterParamsLock.RLock() + repState := b.Core.replicationState + b.Core.clusterParamsLock.RUnlock() + + local := data.Get("local").(bool) + if !local && repState == consts.ReplicationSecondary { + return logical.ErrorResponse("cannot add a non-local mount to a replication secondary"), nil + } + // Get all the options path := data.Get("path").(string) logicalType := data.Get("type").(string) @@ -954,6 +1000,7 @@ func (b *SystemBackend) handleMount( Type: logicalType, Description: description, Config: config, + Local: local, } // Attempt mount @@ -979,6 +1026,10 @@ func handleError( // handleUnmount is used to unmount a path func (b *SystemBackend) handleUnmount( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + b.Core.clusterParamsLock.RLock() + repState := b.Core.replicationState + b.Core.clusterParamsLock.RUnlock() + suffix := strings.TrimPrefix(req.Path, "mounts/") if len(suffix) == 0 { return logical.ErrorResponse("path cannot be blank"), logical.ErrInvalidRequest @@ -986,6 +1037,11 @@ func (b *SystemBackend) handleUnmount( suffix = sanitizeMountPath(suffix) + entry := b.Core.router.MatchingMountEntry(suffix) + if entry != nil && !entry.Local && repState == consts.ReplicationSecondary { + return logical.ErrorResponse("cannot unmount a non-local mount on a replication secondary"), nil + } + // Attempt unmount if existed, err := b.Core.unmount(suffix); existed && err != nil { b.Backend.Logger().Error("sys: unmount failed", "path", suffix, "error", err) @@ -998,6 +1054,10 @@ func (b *SystemBackend) handleUnmount( // handleRemount is used to remount a path func (b *SystemBackend) handleRemount( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + b.Core.clusterParamsLock.RLock() + repState := b.Core.replicationState + b.Core.clusterParamsLock.RUnlock() + // Get the paths fromPath := data.Get("from").(string) toPath := data.Get("to").(string) @@ -1010,6 +1070,11 @@ func (b *SystemBackend) handleRemount( fromPath = sanitizeMountPath(fromPath) toPath = sanitizeMountPath(toPath) + entry := b.Core.router.MatchingMountEntry(fromPath) + if entry != nil && !entry.Local && repState == consts.ReplicationSecondary { + return logical.ErrorResponse("cannot remount a non-local mount on a replication secondary"), nil + } + // Attempt remount if err := b.Core.remount(fromPath, toPath); err != nil { b.Backend.Logger().Error("sys: remount failed", "from_path", fromPath, "to_path", toPath, "error", err) @@ -1095,6 +1160,10 @@ func (b *SystemBackend) handleMountTuneWrite( // handleTuneWriteCommon is used to set config settings on a path func (b *SystemBackend) handleTuneWriteCommon( path string, data *framework.FieldData) (*logical.Response, error) { + b.Core.clusterParamsLock.RLock() + repState := b.Core.replicationState + b.Core.clusterParamsLock.RUnlock() + path = sanitizeMountPath(path) // Prevent protected paths from being changed @@ -1110,6 +1179,9 @@ func (b *SystemBackend) handleTuneWriteCommon( b.Backend.Logger().Error("sys: tune failed: no mount entry found", "path", path) return handleError(fmt.Errorf("sys: tune of path '%s' failed: no mount entry found", path)) } + if mountEntry != nil && !mountEntry.Local && repState == consts.ReplicationSecondary { + return logical.ErrorResponse("cannot tune a non-local mount on a replication secondary"), nil + } var lock *sync.RWMutex switch { @@ -1249,6 +1321,7 @@ func (b *SystemBackend) handleAuthTable( "default_lease_ttl": int64(entry.Config.DefaultLeaseTTL.Seconds()), "max_lease_ttl": int64(entry.Config.MaxLeaseTTL.Seconds()), }, + "local": entry.Local, } resp.Data[entry.Path] = info } @@ -1258,6 +1331,15 @@ func (b *SystemBackend) handleAuthTable( // handleEnableAuth is used to enable a new credential backend func (b *SystemBackend) handleEnableAuth( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + b.Core.clusterParamsLock.RLock() + repState := b.Core.replicationState + b.Core.clusterParamsLock.RUnlock() + + local := data.Get("local").(bool) + if !local && repState == consts.ReplicationSecondary { + return logical.ErrorResponse("cannot add a non-local mount to a replication secondary"), nil + } + // Get all the options path := data.Get("path").(string) logicalType := data.Get("type").(string) @@ -1277,6 +1359,7 @@ func (b *SystemBackend) handleEnableAuth( Path: path, Type: logicalType, Description: description, + Local: local, } // Attempt enabling @@ -1391,6 +1474,7 @@ func (b *SystemBackend) handleAuditTable( "type": entry.Type, "description": entry.Description, "options": entry.Options, + "local": entry.Local, } resp.Data[entry.Path] = info } @@ -1424,6 +1508,15 @@ func (b *SystemBackend) handleAuditHash( // handleEnableAudit is used to enable a new audit backend func (b *SystemBackend) handleEnableAudit( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + b.Core.clusterParamsLock.RLock() + repState := b.Core.replicationState + b.Core.clusterParamsLock.RUnlock() + + local := data.Get("local").(bool) + if !local && repState == consts.ReplicationSecondary { + return logical.ErrorResponse("cannot add a non-local mount to a replication secondary"), nil + } + // Get all the options path := data.Get("path").(string) backendType := data.Get("type").(string) @@ -1447,6 +1540,7 @@ func (b *SystemBackend) handleEnableAudit( Type: backendType, Description: description, Options: optionMap, + Local: local, } // Attempt enabling @@ -1562,6 +1656,13 @@ func (b *SystemBackend) handleKeyStatus( // handleRotate is used to trigger a key rotation func (b *SystemBackend) handleRotate( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + b.Core.clusterParamsLock.RLock() + repState := b.Core.replicationState + b.Core.clusterParamsLock.RUnlock() + if repState == consts.ReplicationSecondary { + return logical.ErrorResponse("cannot rotate on a replication secondary"), nil + } + // Rotate to the new term newTerm, err := b.Core.barrier.Rotate() if err != nil { @@ -1584,6 +1685,17 @@ func (b *SystemBackend) handleRotate( } }) } + + // Write to the canary path, which will force a synchronous truing during + // replication + if err := b.Core.barrier.Put(&Entry{ + Key: coreKeyringCanaryPath, + Value: []byte(fmt.Sprintf("new-rotation-term-%d", newTerm)), + }); err != nil { + b.Core.logger.Error("core: error saving keyring canary", "error", err) + return nil, fmt.Errorf("failed to save keyring canary: %v", err) + } + return nil, nil } @@ -1950,6 +2062,11 @@ west coast. and max_lease_ttl.`, }, + "mount_local": { + `Mark the mount as a local mount, which is not replicated +and is unaffected by replication.`, + }, + "tune_default_lease_ttl": { `The default lease TTL for this mount.`, }, diff --git a/vault/logical_system_test.go b/vault/logical_system_test.go index 62737744b2..71cf59c2a8 100644 --- a/vault/logical_system_test.go +++ b/vault/logical_system_test.go @@ -21,6 +21,7 @@ func TestSystemBackend_RootPaths(t *testing.T) { "audit", "audit/*", "raw/*", + "replication/*", "rotate", "config/auditing/*", } @@ -50,6 +51,7 @@ func TestSystemBackend_mounts(t *testing.T) { "default_lease_ttl": resp.Data["secret/"].(map[string]interface{})["config"].(map[string]interface{})["default_lease_ttl"].(int64), "max_lease_ttl": resp.Data["secret/"].(map[string]interface{})["config"].(map[string]interface{})["max_lease_ttl"].(int64), }, + "local": false, }, "sys/": map[string]interface{}{ "type": "system", @@ -58,6 +60,7 @@ func TestSystemBackend_mounts(t *testing.T) { "default_lease_ttl": resp.Data["sys/"].(map[string]interface{})["config"].(map[string]interface{})["default_lease_ttl"].(int64), "max_lease_ttl": resp.Data["sys/"].(map[string]interface{})["config"].(map[string]interface{})["max_lease_ttl"].(int64), }, + "local": false, }, "cubbyhole/": map[string]interface{}{ "description": "per-token private secret storage", @@ -66,6 +69,7 @@ func TestSystemBackend_mounts(t *testing.T) { "default_lease_ttl": resp.Data["cubbyhole/"].(map[string]interface{})["config"].(map[string]interface{})["default_lease_ttl"].(int64), "max_lease_ttl": resp.Data["cubbyhole/"].(map[string]interface{})["config"].(map[string]interface{})["max_lease_ttl"].(int64), }, + "local": false, }, } if !reflect.DeepEqual(resp.Data, exp) { @@ -580,6 +584,7 @@ func TestSystemBackend_authTable(t *testing.T) { "default_lease_ttl": int64(0), "max_lease_ttl": int64(0), }, + "local": false, }, } if !reflect.DeepEqual(resp.Data, exp) { @@ -843,6 +848,7 @@ func TestSystemBackend_auditTable(t *testing.T) { req.Data["options"] = map[string]interface{}{ "foo": "bar", } + req.Data["local"] = true b.HandleRequest(req) req = logical.TestRequest(t, logical.ReadOperation, "audit") @@ -859,6 +865,7 @@ func TestSystemBackend_auditTable(t *testing.T) { "options": map[string]string{ "foo": "bar", }, + "local": true, }, } if !reflect.DeepEqual(resp.Data, exp) { diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index ab40d4f6e1..018945341b 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -11,7 +11,7 @@