From 07120c2faf474b9ca70b969aadc440a8ce4fb29c Mon Sep 17 00:00:00 2001 From: Becca Petrin Date: Wed, 19 Jun 2019 10:04:49 -0700 Subject: [PATCH] update to latest plugin dependencies --- command/agent/auth/pcf/pcf.go | 14 +- command/agent/pcf_end_to_end_test.go | 8 +- go.mod | 2 +- go.sum | 2 + .../vault-plugin-auth-pcf/.gitignore | 3 - .../hashicorp/vault-plugin-auth-pcf/Makefile | 5 +- .../hashicorp/vault-plugin-auth-pcf/README.md | 319 ++++++++++++++++-- .../vault-plugin-auth-pcf/backend.go | 7 +- .../hashicorp/vault-plugin-auth-pcf/cli.go | 16 +- .../hashicorp/vault-plugin-auth-pcf/go.mod | 1 + .../models/configuration.go | 53 +-- .../vault-plugin-auth-pcf/path_config.go | 126 ++++--- .../vault-plugin-auth-pcf/path_login.go | 112 +++--- .../signatures/version1.go | 61 ++-- .../testing/certificates/generate.go | 93 +++-- .../util/certificates.go | 79 +++++ .../vault-plugin-auth-pcf/util/util.go | 39 +++ vendor/modules.txt | 2 +- 18 files changed, 659 insertions(+), 283 deletions(-) create mode 100644 vendor/github.com/hashicorp/vault-plugin-auth-pcf/util/certificates.go diff --git a/command/agent/auth/pcf/pcf.go b/command/agent/auth/pcf/pcf.go index 115b91bcba..3aa7476218 100644 --- a/command/agent/auth/pcf/pcf.go +++ b/command/agent/auth/pcf/pcf.go @@ -56,19 +56,19 @@ func (p *pcfMethod) Authenticate(ctx context.Context, client *api.Client) (strin } signingTime := time.Now().UTC() signatureData := &signatures.SignatureData{ - SigningTime: signingTime, - Role: p.roleName, - Certificate: string(certBytes), + SigningTime: signingTime, + Role: p.roleName, + CFInstanceCertContents: string(certBytes), } signature, err := signatures.Sign(pathToClientKey, signatureData) if err != nil { return "", nil, err } data := map[string]interface{}{ - "role": p.roleName, - "certificate": string(certBytes), - "signing_time": signingTime.Format(signatures.TimeFormat), - "signature": signature, + "role": p.roleName, + "cf_instance_cert": string(certBytes), + "signing_time": signingTime.Format(signatures.TimeFormat), + "signature": signature, } return fmt.Sprintf("%s/login", p.mountPath), data, nil } diff --git a/command/agent/pcf_end_to_end_test.go b/command/agent/pcf_end_to_end_test.go index d5641b59af..2cf1ab5c91 100644 --- a/command/agent/pcf_end_to_end_test.go +++ b/command/agent/pcf_end_to_end_test.go @@ -70,10 +70,10 @@ func TestPCFEndToEnd(t *testing.T) { // Configure a CA certificate like a Vault operator would in setting up PCF. if _, err := client.Logical().Write("auth/pcf/config", map[string]interface{}{ - "certificates": testPCFCerts.CACertificate, - "pcf_api_addr": mockPCFAPI.URL, - "pcf_username": pcfAPI.AuthUsername, - "pcf_password": pcfAPI.AuthPassword, + "identity_ca_certificates": testPCFCerts.CACertificate, + "pcf_api_addr": mockPCFAPI.URL, + "pcf_username": pcfAPI.AuthUsername, + "pcf_password": pcfAPI.AuthPassword, }); err != nil { t.Fatal(err) } diff --git a/go.mod b/go.mod index 2719bc9b06..5dc3a71230 100644 --- a/go.mod +++ b/go.mod @@ -74,7 +74,7 @@ require ( github.com/hashicorp/vault-plugin-auth-gcp v0.5.1 github.com/hashicorp/vault-plugin-auth-jwt v0.5.1 github.com/hashicorp/vault-plugin-auth-kubernetes v0.5.1 - github.com/hashicorp/vault-plugin-auth-pcf v0.0.0-20190605234735-619218abcd26 + github.com/hashicorp/vault-plugin-auth-pcf v0.0.0-20190619165123-fb996be2877c github.com/hashicorp/vault-plugin-secrets-ad v0.5.1 github.com/hashicorp/vault-plugin-secrets-alicloud v0.5.1 github.com/hashicorp/vault-plugin-secrets-azure v0.5.1 diff --git a/go.sum b/go.sum index 8139b1728e..70155c8bc1 100644 --- a/go.sum +++ b/go.sum @@ -290,6 +290,8 @@ github.com/hashicorp/vault-plugin-auth-pcf v0.0.0-20190524170107-2d769dfedad4 h1 github.com/hashicorp/vault-plugin-auth-pcf v0.0.0-20190524170107-2d769dfedad4/go.mod h1:9866PkjxPBXclbEJBKzVGY60pgVIY9b7qZJ5Fa+p6VY= github.com/hashicorp/vault-plugin-auth-pcf v0.0.0-20190605234735-619218abcd26 h1:mz5YaAFveImGMooFLAW14kdSBH4jVdRnKTQYAz0fEHw= github.com/hashicorp/vault-plugin-auth-pcf v0.0.0-20190605234735-619218abcd26/go.mod h1:9866PkjxPBXclbEJBKzVGY60pgVIY9b7qZJ5Fa+p6VY= +github.com/hashicorp/vault-plugin-auth-pcf v0.0.0-20190619165123-fb996be2877c h1:/g4Yr7pCTfKVqjUUVO4/Pkd3Vmw2TB3znuB4lF7ZNNY= +github.com/hashicorp/vault-plugin-auth-pcf v0.0.0-20190619165123-fb996be2877c/go.mod h1:AjWJZO3nIHzc1inkx57x5qtIIcpi1sejXiwJVcNpjyc= github.com/hashicorp/vault-plugin-secrets-ad v0.5.1 h1:BdiASUZLOvOUs317EnaUNjGxTSw0PYGQA7zJZhDKLC4= github.com/hashicorp/vault-plugin-secrets-ad v0.5.1/go.mod h1:EH9CI8+0aWRBz8eIgGth0QjttmHWlGvn+8ZmX/ZUetE= github.com/hashicorp/vault-plugin-secrets-alicloud v0.5.1 h1:72K91p4uLhT/jgtBq2zV5Wn8ocvny4sAN56XOcTxK1w= diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-pcf/.gitignore b/vendor/github.com/hashicorp/vault-plugin-auth-pcf/.gitignore index 2ee43a22a9..361c9b665a 100644 --- a/vendor/github.com/hashicorp/vault-plugin-auth-pcf/.gitignore +++ b/vendor/github.com/hashicorp/vault-plugin-auth-pcf/.gitignore @@ -20,6 +20,3 @@ cmd/verify/verify pkg* bin* - -# Ignore fake certificates generated for tests -testdata/fake-certificates* diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-pcf/Makefile b/vendor/github.com/hashicorp/vault-plugin-auth-pcf/Makefile index c64242fcc0..7cbc6f608a 100644 --- a/vendor/github.com/hashicorp/vault-plugin-auth-pcf/Makefile +++ b/vendor/github.com/hashicorp/vault-plugin-auth-pcf/Makefile @@ -22,7 +22,7 @@ testshort: fmtcheck generate CGO_ENABLED=0 VAULT_TOKEN= VAULT_ACC= go test -short -tags='$(BUILD_TAGS)' $(TEST) $(TESTARGS) -count=1 -timeout=20m -parallel=4 # test runs the unit tests and vets the code -test: gencerts fmtcheck generate +test: fmtcheck generate CGO_ENABLED=0 VAULT_TOKEN= VAULT_ACC= go test ./... -v -tags='$(BUILD_TAGS)' $(TEST) $(TESTARGS) -count=1 -timeout=20m -parallel=4 testcompile: fmtcheck generate @@ -45,9 +45,6 @@ bootstrap: fmtcheck: @sh -c "'$(CURDIR)/scripts/gofmtcheck.sh'" -gencerts: - @sh -c "'$(CURDIR)/scripts/generate-test-certs.sh'" - fmt: gofmt -w $(GOFMT_FILES) diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-pcf/README.md b/vendor/github.com/hashicorp/vault-plugin-auth-pcf/README.md index 28d37bc753..67831ac18c 100644 --- a/vendor/github.com/hashicorp/vault-plugin-auth-pcf/README.md +++ b/vendor/github.com/hashicorp/vault-plugin-auth-pcf/README.md @@ -3,17 +3,270 @@ This plugin leverages PCF's [App and Container Identity Assurance](https://content.pivotal.io/blog/new-in-pcf-2-1-app-container-identity-assurance-via-automatic-cert-rotation) for authenticating to Vault. +## Known Risks + +This authentication engine uses PCF's instance identity service to authenticate users to Vault. Because PCF +makes its CA certificate and **private key** available to certain users at any time, it's possible for someone +with access to them to self-issue identity certificates that meet the criteria for a Vault role, allowing +them to gain unintended access to Vault. + +For this reason, we recommend that if you choose this auth method, you **carefully guard access to +the private key** for your instance identity CA certificate. In CredHub, it can be obtained through the +following call: `$ credhub get -n /cf/diego-instance-identity-root-ca`. + +Take extra steps to limit access to that path in CredHub, whether it be through use of CredHub's ACL system, +or through carefully limiting the users who can access CredHub. + ## Getting Started +### Obtaining Your Instance Identity CA Certificate + +In most versions of PCF, instance identity is enabled out-of-the-box. Check by pulling your CA certificate, +which you'll need to configure this auth engine. There are undoubtedly multiple ways to do this, but this +is how we did it. + +#### From CF Dev + +``` +$ bosh int --path /diego_instance_identity_ca ~/.cfdev/state/bosh/creds.yml +``` + +#### From CredHub + +[Install and authenticate to the PCF command line tool](https://docs.pivotal.io/tiledev/2-2/pcf-command.html), +and [install jq](https://stedolan.github.io/jq/). + +Get the credentials you'll use for CredHub: +``` +$ pcf settings | jq '.products[0].director_credhub_client_credentials' +``` + +SSH into your Ops Manager VM: +``` +ssh -i ops_mgr.pem ubuntu@$OPS_MGR_URL +``` +Please note that the above `OPS_MGR_URL` shouldn't be prepended with `https://`. + +Log in to Credhub using the credentials you obtained earlier: +``` +$ credhub login --client-name=director_to_credhub --client-secret=CoJPkrsYi3c-Fx2QHEEDyaEEUuOfYMzw +``` +6. Retrieve the CA information: +``` +$ credhub get -n /cf/diego-instance-identity-root-ca +``` +7. You'll receive a response like: +``` +id: be2bd996-1d35-443b-b81c-90095024d5e7 +name: /cf/diego-instance-identity-root-ca +type: certificate +value: + ca: | + -----BEGIN CERTIFICATE----- + MIIDNDCCAhygAwIBAgITPqTy1qvfHNEVuxsl9l1glY85OTANBgkqhkiG9w0BAQsF + ADAqMSgwJgYDVQQDEx9EaWVnbyBJbnN0YW5jZSBJZGVudGl0eSBSb290IENBMB4X + DTE5MDYwNjA5MTIwMVoXDTIyMDYwNTA5MTIwMVowKjEoMCYGA1UEAxMfRGllZ28g + SW5zdGFuY2UgSWRlbnRpdHkgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP + ADCCAQoCggEBALa8xGDYT/q3UzEKAsLDajhuHxPpIPFlCXwp6u8U5Qrf427Xof7n + rXRKzRu3g7E20U/OwzgBi3VZs8T29JGNWeA2k0HtX8oQ+Wc8Qngz9M8t1h9SZlx5 + fGfxPt3x7xozaIGJ8p4HKQH1ZlirL7dzun7Y+7m6Ey8cMVsepqUs64r8+KpCbxKJ + rV04qtTNlr0LG3yOxSHlip+DDvUVL3jSFz/JDWxwCymiFBAh0QjG1LKp2FisURoX + GY+HJbf2StpK3i4dYnxQXQlMDpipozK7WFxv3gH4Q6YMZvlmIPidAF8FxfDIsYcq + TgQ5q0pr9mbu8oKbZ74vyZMqiy+r9vLhbu0CAwEAAaNTMFEwHQYDVR0OBBYEFAHf + pwqBhZ8/A6ZAvU+p5JPz/omjMB8GA1UdIwQYMBaAFAHfpwqBhZ8/A6ZAvU+p5JPz + /omjMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADuDJev+6bOC + v7t9SS4Nd/zeREuF9IKsHDHrYUZBIO1aBQbOO1iDtL4VA3LBEx6fOgN5fbxroUsz + X9/6PtxLe+5U8i5MOztK+OxxPrtDfnblXVb6IW4EKhTnWesS7R2WnOWtzqRQXKFU + voBn3QckLV1o9eqzYIE/aob4z0GaVanA9PSzzbVPsX79RCD1B7NmV0cKEQ7IrCrh + L7ElDV/GlNrtVdHjY0mwz9iu+0YJvxvcHDTERi106b28KXzJz+P5/hyg2wqRXzdI + faXAjW0kuq5nxyJUALwxD/8pz77uNt4w6WfJoSDM6XrAIhh15K3tZg9EzBmAZ/5D + jK0RcmCyaXw= + -----END CERTIFICATE----- + certificate: | + -----BEGIN CERTIFICATE----- + MIIDNDCCAhygAwIBAgITPqTy1qvfHNEVuxsl9l1glY85OTANBgkqhkiG9w0BAQsF + ADAqMSgwJgYDVQQDEx9EaWVnbyBJbnN0YW5jZSBJZGVudGl0eSBSb290IENBMB4X + DTE5MDYwNjA5MTIwMVoXDTIyMDYwNTA5MTIwMVowKjEoMCYGA1UEAxMfRGllZ28g + SW5zdGFuY2UgSWRlbnRpdHkgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP + ADCCAQoCggEBALa8xGDYT/q3UzEKAsLDajhuHxPpIPFlCXwp6u8U5Qrf427Xof7n + rXRKzRu3g7E20U/OwzgBi3VZs8T29JGNWeA2k0HtX8oQ+Wc8Qngz9M8t1h9SZlx5 + fGfxPt3x7xozaIGJ8p4HKQH1ZlirL7dzun7Y+7m6Ey8cMVsepqUs64r8+KpCbxKJ + rV04qtTNlr0LG3yOxSHlip+DDvUVL3jSFz/JDWxwCymiFBAh0QjG1LKp2FisURoX + GY+HJbf2StpK3i4dYnxQXQlMDpipozK7WFxv3gH4Q6YMZvlmIPidAF8FxfDIsYcq + TgQ5q0pr9mbu8oKbZ74vyZMqiy+r9vLhbu0CAwEAAaNTMFEwHQYDVR0OBBYEFAHf + pwqBhZ8/A6ZAvU+p5JPz/omjMB8GA1UdIwQYMBaAFAHfpwqBhZ8/A6ZAvU+p5JPz + /omjMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADuDJev+6bOC + v7t9SS4Nd/zeREuF9IKsHDHrYUZBIO1aBQbOO1iDtL4VA3LBEx6fOgN5fbxroUsz + X9/6PtxLe+5U8i5MOztK+OxxPrtDfnblXVb6IW4EKhTnWesS7R2WnOWtzqRQXKFU + voBn3QckLV1o9eqzYIE/aob4z0GaVanA9PSzzbVPsX79RCD1B7NmV0cKEQ7IrCrh + L7ElDV/GlNrtVdHjY0mwz9iu+0YJvxvcHDTERi106b28KXzJz+P5/hyg2wqRXzdI + faXAjW0kuq5nxyJUALwxD/8pz77uNt4w6WfJoSDM6XrAIhh15K3tZg9EzBmAZ/5D + jK0RcmCyaXw= + -----END CERTIFICATE----- + private_key: | + -----BEGIN RSA PRIVATE KEY----- + MIIEpAIBAAKCAQEAtrzEYNhP+rdTMQoCwsNqOG4fE+kg8WUJfCnq7xTlCt/jbteh + /uetdErNG7eDsTbRT87DOAGLdVmzxPb0kY1Z4DaTQe1fyhD5ZzxCeDP0zy3WH1Jm + XHl8Z/E+3fHvGjNogYnyngcpAfVmWKsvt3O6ftj7uboTLxwxWx6mpSzrivz4qkJv + EomtXTiq1M2WvQsbfI7FIeWKn4MO9RUveNIXP8kNbHALKaIUECHRCMbUsqnYWKxR + GhcZj4clt/ZK2kreLh1ifFBdCUwOmKmjMrtYXG/eAfhDpgxm+WYg+J0AXwXF8Mix + hypOBDmrSmv2Zu7ygptnvi/JkyqLL6v28uFu7QIDAQABAoIBACGIPhjvWK3PGirz + hVIr/b/hJT7IFs11Fup73qqEkQsPznI2i3l1FfUzDLQ7VqUcRAh7DoOmdOrRzRUl + o/dZktZ77UW5w0wXFU0GV8Qq9I9X/+S7gCEUAeoo8LpVfOS37kNnBuhMtA+x8lfv + AdCOIfjI5FhOdtq8N6pa04WX2pkkRzQkIpneRcLPqq1WwKZK1o8zxCnbP+4SI8yo + dTB2ldDY+1vusMYoFch2IsPDMCxVUpAYOoO0jvyOM4cqm0m7P+Tb6Qy19GG50ZfC + PlEK76YTOurdirGWdnGC+JEf4smrGgIvJGKEb55/qbqPow/o6Bf4XVXnOHaboW/W + Mu4cGFkCgYEA2JaQxFF51yuz4W7/VFZCCix3woPH12fZ3fz7aVMUfn4WQwMbNLVa + 7G4gdRclReOs5FLM7jPqiTxDckXnoWRc5Ff9Xn4c2ioXrqzXr5V4qJ4GAWxAx0uM + w1u5ZpVL2HO12pat74MnYw7EJ65oznQNFSC1FAGn5BJ9f5HFk4X3ngMCgYEA1/1Q + XmAk1XJUQ0RP0EehwNipZQPhodGmrqHBGED+N/8+eRMo4shi//EcPXZ222j9kqAE + inPA9qaDxhBjgMt+JBFkj/bmTO/Yz8XusBBa5YlN9Ev30zlO+dRlM41/piluPTzf + vNQuzyNIzl2Gzd71R1TcuFWIDxn8BR0/cBA/5E8CgYAT7m8uEc1jlrr8AOnwSevT + 4dm3hccLNJxhCFnejG2zYkkMK6oCRLo0TcIg5Ftivhv3+wKu3Qo1TN1sE7DIMmM2 + BD7lxjdDgGIjifZjSx8KbVhiIyMm8/XlOHisTwrmxWcz0W/6PZiPThmRCUTN0vIt + QpBHYgugOm9gIPsMo2RxHwKBgQDOUDjZvUrR3GCi1HjMwe+/bvX3+MopMULfYsE4 + srRittxs+KFAZxsx0ZUhHKySDurQiSttOP6kXBBZPERfvYFjYH3HipcX/K8EYNQL + t8OrqAkfhwVV7VMEDx8QLGQ3SzHzKteo3qFL2S9teCcRNZzjoysmpQTPMAnstLBp + EgyFvwKBgQDObNn/Kmfwi6TuGhIjLtBuUEhp5n4EUtysTUZs/15h02MWOfI8CCvm + xWb6/vZrVggxGlZgZtKy9+COPVpEMFaVdwq9uq4lW77sSBwGIwfzHd1CIjce6mSg + P5+wO3aTgvr4n8D5NyWcnYPJKRQzqWHHnfk+9TQA1l0g3/yQXfCx2A== + -----END RSA PRIVATE KEY----- +version_created_at: "2019-06-06T09:12:01Z" +``` + +From that response, copy the first certificate (under `ca: |`) and place +it into its own separate file using a plain text editor like [Sublime](https://www.sublimetext.com/). +The following instructions assume you name the file `ca.crt`. +Remove any tabs before each line, and any trailing space or lines. When +complete, your CA certificate should look like this: + +``` +$ cat ca.crt +-----BEGIN CERTIFICATE----- +MIIDNDCCAhygAwIBAgITPqTy1qvfHNEVuxsl9l1glY85OTANBgkqhkiG9w0BAQsF +ADAqMSgwJgYDVQQDEx9EaWVnbyBJbnN0YW5jZSBJZGVudGl0eSBSb290IENBMB4X +DTE5MDYwNjA5MTIwMVoXDTIyMDYwNTA5MTIwMVowKjEoMCYGA1UEAxMfRGllZ28g +SW5zdGFuY2UgSWRlbnRpdHkgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBALa8xGDYT/q3UzEKAsLDajhuHxPpIPFlCXwp6u8U5Qrf427Xof7n +rXRKzRu3g7E20U/OwzgBi3VZs8T29JGNWeA2k0HtX8oQ+Wc8Qngz9M8t1h9SZlx5 +fGfxPt3x7xozaIGJ8p4HKQH1ZlirL7dzun7Y+7m6Ey8cMVsepqUs64r8+KpCbxKJ +rV04qtTNlr0LG3yOxSHlip+DDvUVL3jSFz/JDWxwCymiFBAh0QjG1LKp2FisURoX +GY+HJbf2StpK3i4dYnxQXQlMDpipozK7WFxv3gH4Q6YMZvlmIPidAF8FxfDIsYcq +TgQ5q0pr9mbu8oKbZ74vyZMqiy+r9vLhbu0CAwEAAaNTMFEwHQYDVR0OBBYEFAHf +pwqBhZ8/A6ZAvU+p5JPz/omjMB8GA1UdIwQYMBaAFAHfpwqBhZ8/A6ZAvU+p5JPz +/omjMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADuDJev+6bOC +v7t9SS4Nd/zeREuF9IKsHDHrYUZBIO1aBQbOO1iDtL4VA3LBEx6fOgN5fbxroUsz +X9/6PtxLe+5U8i5MOztK+OxxPrtDfnblXVb6IW4EKhTnWesS7R2WnOWtzqRQXKFU +voBn3QckLV1o9eqzYIE/aob4z0GaVanA9PSzzbVPsX79RCD1B7NmV0cKEQ7IrCrh +L7ElDV/GlNrtVdHjY0mwz9iu+0YJvxvcHDTERi106b28KXzJz+P5/hyg2wqRXzdI +faXAjW0kuq5nxyJUALwxD/8pz77uNt4w6WfJoSDM6XrAIhh15K3tZg9EzBmAZ/5D +jK0RcmCyaXw= +-----END CERTIFICATE----- +``` + +Verify that this certificate can be properly parsed like so: + +``` +$ openssl x509 -in ca.crt -text -noout +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 3e:a4:f2:d6:ab:df:1c:d1:15:bb:1b:25:f6:5d:60:95:8f:39:39 + Signature Algorithm: sha256WithRSAEncryption + Issuer: CN=Diego Instance Identity Root CA + Validity + Not Before: Jun 6 09:12:01 2019 GMT + Not After : Jun 5 09:12:01 2022 GMT + Subject: CN=Diego Instance Identity Root CA + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:b6:bc:c4:60:d8:4f:fa:b7:53:31:0a:02:c2:c3: + 6a:38:6e:1f:13:e9:20:f1:65:09:7c:29:ea:ef:14: + e5:0a:df:e3:6e:d7:a1:fe:e7:ad:74:4a:cd:1b:b7: + 83:b1:36:d1:4f:ce:c3:38:01:8b:75:59:b3:c4:f6: + f4:91:8d:59:e0:36:93:41:ed:5f:ca:10:f9:67:3c: + 42:78:33:f4:cf:2d:d6:1f:52:66:5c:79:7c:67:f1: + 3e:dd:f1:ef:1a:33:68:81:89:f2:9e:07:29:01:f5: + 66:58:ab:2f:b7:73:ba:7e:d8:fb:b9:ba:13:2f:1c: + 31:5b:1e:a6:a5:2c:eb:8a:fc:f8:aa:42:6f:12:89: + ad:5d:38:aa:d4:cd:96:bd:0b:1b:7c:8e:c5:21:e5: + 8a:9f:83:0e:f5:15:2f:78:d2:17:3f:c9:0d:6c:70: + 0b:29:a2:14:10:21:d1:08:c6:d4:b2:a9:d8:58:ac: + 51:1a:17:19:8f:87:25:b7:f6:4a:da:4a:de:2e:1d: + 62:7c:50:5d:09:4c:0e:98:a9:a3:32:bb:58:5c:6f: + de:01:f8:43:a6:0c:66:f9:66:20:f8:9d:00:5f:05: + c5:f0:c8:b1:87:2a:4e:04:39:ab:4a:6b:f6:66:ee: + f2:82:9b:67:be:2f:c9:93:2a:8b:2f:ab:f6:f2:e1: + 6e:ed + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Subject Key Identifier: + 01:DF:A7:0A:81:85:9F:3F:03:A6:40:BD:4F:A9:E4:93:F3:FE:89:A3 + X509v3 Authority Key Identifier: + keyid:01:DF:A7:0A:81:85:9F:3F:03:A6:40:BD:4F:A9:E4:93:F3:FE:89:A3 + + X509v3 Basic Constraints: critical + CA:TRUE + Signature Algorithm: sha256WithRSAEncryption + 3b:83:25:eb:fe:e9:b3:82:bf:bb:7d:49:2e:0d:77:fc:de:44: + 4b:85:f4:82:ac:1c:31:eb:61:46:41:20:ed:5a:05:06:ce:3b: + 58:83:b4:be:15:03:72:c1:13:1e:9f:3a:03:79:7d:bc:6b:a1: + 4b:33:5f:df:fa:3e:dc:4b:7b:ee:54:f2:2e:4c:3b:3b:4a:f8: + ec:71:3e:bb:43:7e:76:e5:5d:56:fa:21:6e:04:2a:14:e7:59: + eb:12:ed:1d:96:9c:e5:ad:ce:a4:50:5c:a1:54:be:80:67:dd: + 07:24:2d:5d:68:f5:ea:b3:60:81:3f:6a:86:f8:cf:41:9a:55: + a9:c0:f4:f4:b3:cd:b5:4f:b1:7e:fd:44:20:f5:07:b3:66:57: + 47:0a:11:0e:c8:ac:2a:e1:2f:b1:25:0d:5f:c6:94:da:ed:55: + d1:e3:63:49:b0:cf:d8:ae:fb:46:09:bf:1b:dc:1c:34:c4:46: + 2d:74:e9:bd:bc:29:7c:c9:cf:e3:f9:fe:1c:a0:db:0a:91:5f: + 37:48:7d:a5:c0:8d:6d:24:ba:ae:67:c7:22:54:00:bc:31:0f: + ff:29:cf:be:ee:36:de:30:e9:67:c9:a1:20:cc:e9:7a:c0:22: + 18:75:e4:ad:ed:66:0f:44:cc:19:80:67:fe:43:8c:ad:11:72: + 60:b2:69:7c +``` + +Congratulations! You have obtained the CA certificate you'll use for configuring +this auth engine. + +### Obtaining Your API Credentials + +From the directory where you added `metadata` in the previous step to authenticate to the pcf command-line +tool, run the following commands: + +``` +$ pcf target +$ cf api +``` + +The api endpoint given will be used for configuring this Vault auth method. + +This plugin was tested with Org Manager level permissions, but lower level permissions may be usable. +``` +$ cf create-user vault pa55word +$ cf orgs +$ cf org-users my-example-org +$ cf set-org-role Alice my-example-org OrgManager +``` + +Since the PCF API tends to use a self-signed certificate, you'll also need to configure +Vault to trust that certificate. You can obtain its API certificate via: + +``` +openssl s_client -showcerts -servername domain.com -connect domain.com:443 +``` + +You'll see a certificate outputted as part of the response, which should be broken +out into a separate well-formatted file like the `ca.crt` above, and used for the +`pcf_api_trusted_certificates` field. + +## Downloading the Plugin + - `$ git clone git@github.com:hashicorp/vault-plugin-auth-pcf.git` - `$ cd vault-plugin-auth-pcf` - `$ PCF_HOME=$(pwd)` -- `$ make test` -- `$ make tools` - -`$ make test` is run above to generate valid fake certificates in your `testdata/fake-certificates` folder. -`$ make tools` is run above to install a number of tools that have been placed here in the `cmd` directory -to make your life easier. Running the command will place them in your `$GOPATH/bin` directory. ## Sample Usage @@ -21,22 +274,21 @@ Please note that this example uses `generate-signature`, a tool installed throug First, enable the PCF auth engine. ``` -$ vault auth enable vault-plugin-auth-pcf +$ vault auth enable pcf ``` -Next, configure the plugin. In the `config` call below, the `certificates` configured is intended to be the CA -certificate that has been configured as the `diego.executor.instance_identity_ca_cert` in your environment. For -instructions on configuring this, see PCF's -[Enabling Instance Identity](https://docs.cloudfoundry.org/adminguide/instance-identity.html). +Next, configure the plugin. In the `config` call below, `certificates` is intended to be the instance +identity CA certificate you pulled above. In the CF Dev environment the default API address is `https://api.dev.cfdev.sh`. The default username and password are `admin`, `admin`. In a production environment, these attributes will vary. ``` -$ vault write auth/vault-plugin-auth-pcf/config \ - certificates=@$PCF_HOME/testdata/fake-certificates/ca.crt \ - pcf_api_addr=http://127.0.0.1:33671 \ - pcf_username=username \ - pcf_password=password +$ vault write auth/pcf/config \ + certificates=@ca.crt \ + pcf_api_addr=https://api.dev.cfdev.sh \ + pcf_username=admin \ + pcf_password=admin \ + pcf_api_trusted_certificates=@pcfapi.crt ``` Then, add a role that will be used to grant specific Vault policies to those logging in with it. When a constraint like @@ -47,15 +299,11 @@ configuring as many bound parameters as possible. Also, by default, the IP address on the certificate presented at login must match that of the caller. However, if your callers tend to be proxied, this may not work for you. If that's the case, set `disable_ip_matching` to true. ``` -$ vault write auth/vault-plugin-auth-pcf/roles/test-role \ +$ vault write auth/pcf/roles/test-role \ bound_application_ids=2d3e834a-3a25-4591-974c-fa5626d5d0a1 \ bound_space_ids=3d2eba6b-ef19-44d5-91dd-1975b0db5cc9 \ bound_organization_ids=34a878d0-c2f9-4521-ba73-a9f664e82c7bf \ - bound_instance_ids=1bf2e7f6-2d1d-41ec-501c-c70 \ - policies=foo-policies \ - ttl=86400s \ - max_ttl=86400s \ - period=86400s + policies=foo-policies ``` Logging in is intended to be performed using your `CF_INSTANCE_CERT` and `CF_INSTANCE_KEY`. This is an example of how @@ -63,13 +311,7 @@ it can be done. ``` $ export CF_INSTANCE_CERT=$PCF_HOME/testdata/fake-certificates/instance.crt $ export CF_INSTANCE_KEY=$PCF_HOME/testdata/fake-certificates/instance.key -$ export SIGNING_TIME=$(date -u) -$ export ROLE='test-role' -$ vault write auth/vault-plugin-auth-pcf/login \ - role=$ROLE \ - certificate=@$CF_INSTANCE_CERT \ - signing-time="$SIGNING_TIME" \ - signature=$(generate-signature) +$ vault login -method=pcf role=test-role ``` ### Updating the CA Certificate @@ -94,6 +336,21 @@ login will succeed. ## Troubleshooting +### Obtaining a Certificate Error from the PCF API + +When configuring this plugin, you may encounter an error like: +``` +Error writing data to auth/pcf/config: Error making API request. + +URL: PUT http://127.0.0.1:8200/v1/auth/pcf/config +Code: 500. Errors: + +* 1 error occurred: + * unable to establish an initial connection to the PCF API: Could not get api /v2/info: Get https://api.sys.lagunaniguel.cf-app.com/v2/info: x509: certificate signed by unknown authority +``` + +To resolve this error, review instructions above regarding setting the `pcf_api_trusted_certificates` field. + ### verify-certs This tool, installed by `make tools`, is for verifying that your CA certificate, client certificate, and client @@ -103,9 +360,7 @@ debugging authentication problems that may be related to your certificates, it's ``` verify-certs -ca-cert=local/path/to/ca.crt -instance-cert=local/path/to/instance.crt -instance-key=local/path/to/instance.key ``` -The `ca-cert` should be the cert that was used to issue the given client certificate. In the CF Dev environment, -it can be obtained via `$ bosh int --path /diego_instance_identity_ca ~/.cfdev/state/bosh/creds.yml`. In a prod -environment, it should be available through the Ops Manager API. +The `ca-cert` should be the cert that was used to issue the given client certificate. The `instance-cert` given should be the value for the `CF_INSTANCE_CERT` variable in the PCF environment you're using, and the `instance-key` should be the value for the `CF_INSTANCE_KEY`. diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-pcf/backend.go b/vendor/github.com/hashicorp/vault-plugin-auth-pcf/backend.go index d6920a39e9..a2ca00cb6a 100644 --- a/vendor/github.com/hashicorp/vault-plugin-auth-pcf/backend.go +++ b/vendor/github.com/hashicorp/vault-plugin-auth-pcf/backend.go @@ -2,7 +2,7 @@ package pcf import ( "context" - "github.com/hashicorp/go-hclog" + "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/logical" ) @@ -16,9 +16,7 @@ const ( ) func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) { - b := &backend{ - logger: hclog.Default(), - } + b := &backend{} b.Backend = &framework.Backend{ AuthRenew: b.pathLoginRenew, Help: backendHelp, @@ -42,7 +40,6 @@ func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, type backend struct { *framework.Backend - logger hclog.Logger } const backendHelp = ` diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-pcf/cli.go b/vendor/github.com/hashicorp/vault-plugin-auth-pcf/cli.go index b0a91ed721..a7daab305d 100644 --- a/vendor/github.com/hashicorp/vault-plugin-auth-pcf/cli.go +++ b/vendor/github.com/hashicorp/vault-plugin-auth-pcf/cli.go @@ -45,13 +45,13 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro if err != nil { return nil, err } - certificate := string(certBytes) + cfInstanceCertContents := string(certBytes) signingTime := time.Now().UTC() signatureData := &signatures.SignatureData{ - SigningTime: signingTime, - Role: role, - Certificate: certificate, + SigningTime: signingTime, + Role: role, + CFInstanceCertContents: cfInstanceCertContents, } signature, err := signatures.Sign(pathToInstanceKey, signatureData) if err != nil { @@ -59,10 +59,10 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro } loginData := map[string]interface{}{ - "role": role, - "certificate": certificate, - "signing_time": signingTime.Format(signatures.TimeFormat), - "signature": signature, + "role": role, + "cf_instance_cert": cfInstanceCertContents, + "signing_time": signingTime.Format(signatures.TimeFormat), + "signature": signature, } path := fmt.Sprintf("auth/%s/login", mount) diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-pcf/go.mod b/vendor/github.com/hashicorp/vault-plugin-auth-pcf/go.mod index a969ad8cdd..d3126afa82 100644 --- a/vendor/github.com/hashicorp/vault-plugin-auth-pcf/go.mod +++ b/vendor/github.com/hashicorp/vault-plugin-auth-pcf/go.mod @@ -4,6 +4,7 @@ go 1.12 require ( github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 + github.com/hashicorp/go-cleanhttp v0.5.1 github.com/hashicorp/go-hclog v0.9.2 github.com/hashicorp/go-multierror v1.0.0 github.com/hashicorp/go-sockaddr v1.0.2 diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-pcf/models/configuration.go b/vendor/github.com/hashicorp/vault-plugin-auth-pcf/models/configuration.go index ee78bf0036..9a151cfec7 100644 --- a/vendor/github.com/hashicorp/vault-plugin-auth-pcf/models/configuration.go +++ b/vendor/github.com/hashicorp/vault-plugin-auth-pcf/models/configuration.go @@ -1,35 +1,14 @@ package models -import ( - "crypto/x509" - "errors" - "fmt" -) +import "time" -// NewConfiguration is the way a Configuration is intended to be obtained. It ensures the -// given certificates are valid and prepares a CA certificate pool to be used for client -// certificate verification. -func NewConfiguration(certificates []string, pcfAPIAddr, pcfUsername, pcfPassword string) (*Configuration, error) { - config := &Configuration{ - Certificates: certificates, - PCFAPIAddr: pcfAPIAddr, - PCFUsername: pcfUsername, - PCFPassword: pcfPassword, - } - pool := x509.NewCertPool() - for _, certificate := range certificates { - if ok := pool.AppendCertsFromPEM([]byte(certificate)); !ok { - return nil, fmt.Errorf("couldn't append CA certificate: %s", certificate) - } - } - config.verifyOpts = &x509.VerifyOptions{Roots: pool} - return config, nil -} - -// Configuration is not intended to by directly instantiated; please use NewConfiguration. +// Configuration is the config as it's reflected in Vault's storage system. type Configuration struct { - // Certificates are the CA certificates that should be used for verifying client certificates. - Certificates []string `json:"certificates"` + // IdentityCACertificates are the CA certificates that should be used for verifying client certificates. + IdentityCACertificates []string `json:"identity_ca_certificates"` + + // IdentityCACertificates that, if presented by the PCF API, should be trusted. + PCFAPICertificates []string `json:"pcf_api_trusted_certificates"` // PCFAPIAddr is the address of PCF's API, ex: "https://api.dev.cfdev.sh" or "http://127.0.0.1:33671" PCFAPIAddr string `json:"pcf_api_addr"` @@ -40,17 +19,11 @@ type Configuration struct { // The password for the PCF API. PCFPassword string `json:"pcf_password"` - // verifyOpts is intentionally lower-cased so it won't be stored in JSON. - // Instead, this struct is expected to be created from NewConfiguration - // so that it'll populate this field. - verifyOpts *x509.VerifyOptions -} + // The maximum seconds old a login request's signing time can be. + // This is configurable because in some test environments we found as much as 2 hours of clock drift. + LoginMaxSecOld time.Duration `json:"login_max_seconds_old"` -// VerifyOpts returns the options that can be used for verifying client certificates, -// including the CA certificate pool. -func (c *Configuration) VerifyOpts() (x509.VerifyOptions, error) { - if c.verifyOpts == nil { - return x509.VerifyOptions{}, errors.New("verify options are unset") - } - return *c.verifyOpts, nil + // The maximum seconds ahead a login request's signing time can be. + // This is configurable because in some test environments we found as much as 2 hours of clock drift. + LoginMaxSecAhead time.Duration `json:"login_max_seconds_ahead"` } diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-pcf/path_config.go b/vendor/github.com/hashicorp/vault-plugin-auth-pcf/path_config.go index 10e715939a..72b7a2eccf 100644 --- a/vendor/github.com/hashicorp/vault-plugin-auth-pcf/path_config.go +++ b/vendor/github.com/hashicorp/vault-plugin-auth-pcf/path_config.go @@ -4,9 +4,10 @@ import ( "context" "fmt" "strings" + "time" - "github.com/cloudfoundry-community/go-cfclient" "github.com/hashicorp/vault-plugin-auth-pcf/models" + "github.com/hashicorp/vault-plugin-auth-pcf/util" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/logical" ) @@ -17,10 +18,18 @@ func (b *backend) pathConfig() *framework.Path { return &framework.Path{ Pattern: "config", Fields: map[string]*framework.FieldSchema{ - "certificates": { - Required: true, - Type: framework.TypeStringSlice, - Description: "The PEM-format CA certificates.", + "identity_ca_certificates": { + Required: true, + Type: framework.TypeStringSlice, + DisplayName: "Identity CA Certificates", + DisplayValue: `-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----`, + Description: "The PEM-format CA certificates that are required to have issued the instance certificates presented for logging in.", + }, + "pcf_api_trusted_certificates": { + Type: framework.TypeStringSlice, + DisplayName: "PCF API Trusted IdentityCACertificates", + DisplayValue: `-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----`, + Description: "The PEM-format CA certificates that are acceptable for the PCF API to present.", }, "pcf_api_addr": { Required: true, @@ -44,6 +53,22 @@ func (b *backend) pathConfig() *framework.Path { Description: "The password for PCF’s API.", DisplaySensitive: true, }, + "login_max_seconds_old": { + Type: framework.TypeDurationSecond, + DisplayName: "Login Max Seconds Old", + DisplayValue: "300", + Description: `Duration in seconds for the maximum acceptable age of a "signing_time". Useful for clock drift. +Set low to reduce the opportunity for replay attacks.`, + Default: 300, + }, + "login_max_seconds_ahead": { + Type: framework.TypeInt, + DisplayName: "Login Max Seconds Ahead", + DisplayValue: "60", + Description: `Duration in seconds for the maximum acceptable length in the future a "signing_time" can be. Useful for clock drift. +Set low to reduce the opportunity for replay attacks.`, + Default: 60, + }, }, Operations: map[logical.Operation]framework.OperationHandler{ logical.CreateOperation: &framework.PathOperation{ @@ -71,9 +96,9 @@ func (b *backend) operationConfigCreateUpdate(ctx context.Context, req *logical. } if config == nil { // They're creating a config. - certificates := data.Get("certificates").([]string) - if len(certificates) == 0 { - return logical.ErrorResponse("'certificates' is required"), nil + identityCACerts := data.Get("identity_ca_certificates").([]string) + if len(identityCACerts) == 0 { + return logical.ErrorResponse("'identity_ca_certificates' is required"), nil } pcfApiAddr := data.Get("pcf_api_addr").(string) if pcfApiAddr == "" { @@ -87,27 +112,35 @@ func (b *backend) operationConfigCreateUpdate(ctx context.Context, req *logical. if pcfPassword == "" { return logical.ErrorResponse("'pcf_password' is required"), nil } - config, err = models.NewConfiguration(certificates, pcfApiAddr, pcfUsername, pcfPassword) - if err != nil { - return logical.ErrorResponse(err.Error()), nil + pcfApiCertificates := data.Get("pcf_api_trusted_certificates").([]string) + + // Default this to 5 minutes. + loginMaxSecOld := 300 * time.Second + if raw, ok := data.GetOk("login_max_seconds_old"); ok { + loginMaxSecOld = time.Duration(raw.(int)) * time.Second + } + + // Default this to 1 minute. + loginMaxSecAhead := 60 * time.Second + if raw, ok := data.GetOk("login_max_seconds_ahead"); ok { + loginMaxSecAhead = time.Duration(raw.(int)) * time.Second + } + config = &models.Configuration{ + IdentityCACertificates: identityCACerts, + PCFAPICertificates: pcfApiCertificates, + PCFAPIAddr: pcfApiAddr, + PCFUsername: pcfUsername, + PCFPassword: pcfPassword, + LoginMaxSecOld: loginMaxSecOld, + LoginMaxSecAhead: loginMaxSecAhead, } } else { // They're updating a config. Only update the fields that have been sent in the call. - if raw, ok := data.GetOk("certificates"); ok { - switch v := raw.(type) { - case []interface{}: - certificates := make([]string, len(v)) - for _, certificateIfc := range v { - certificate, ok := certificateIfc.(string) - if !ok { - continue - } - certificates = append(certificates, certificate) - } - config.Certificates = certificates - case string: - config.Certificates = []string{v} - } + if raw, ok := data.GetOk("identity_ca_certificates"); ok { + config.IdentityCACertificates = raw.([]string) + } + if raw, ok := data.GetOk("pcf_api_trusted_certificates"); ok { + config.PCFAPICertificates = raw.([]string) } if raw, ok := data.GetOk("pcf_api_addr"); ok { config.PCFAPIAddr = raw.(string) @@ -118,17 +151,19 @@ func (b *backend) operationConfigCreateUpdate(ctx context.Context, req *logical. if raw, ok := data.GetOk("pcf_password"); ok { config.PCFPassword = raw.(string) } + if raw, ok := data.GetOk("login_max_seconds_old"); ok { + config.LoginMaxSecOld = time.Duration(raw.(int)) * time.Second + } + if raw, ok := data.GetOk("login_max_seconds_ahead"); ok { + config.LoginMaxSecAhead = time.Duration(raw.(int)) * time.Second + } } // To give early and explicit feedback, make sure the config works by executing a test call // and checking that the API version is supported. If they don't have API v2 running, we would - // probably expect a timeout of some sort below because it's first called in the NewClient + // probably expect a timeout of some sort below because it's first called in the NewPCFClient // method. - client, err := cfclient.NewClient(&cfclient.Config{ - ApiAddress: config.PCFAPIAddr, - Username: config.PCFUsername, - Password: config.PCFPassword, - }) + client, err := util.NewPCFClient(config) if err != nil { return nil, fmt.Errorf("unable to establish an initial connection to the PCF API: %s", err) } @@ -160,9 +195,12 @@ func (b *backend) operationConfigRead(ctx context.Context, req *logical.Request, } return &logical.Response{ Data: map[string]interface{}{ - "certificates": config.Certificates, - "pcf_api_addr": config.PCFAPIAddr, - "pcf_username": config.PCFUsername, + "identity_ca_certificates": config.IdentityCACertificates, + "pcf_api_trusted_certificates": config.PCFAPICertificates, + "pcf_api_addr": config.PCFAPIAddr, + "pcf_username": config.PCFUsername, + "login_max_seconds_old": config.LoginMaxSecOld / time.Second, + "login_max_seconds_ahead": config.LoginMaxSecAhead / time.Second, }, }, nil } @@ -183,22 +221,8 @@ func config(ctx context.Context, storage logical.Storage) (*models.Configuration if entry == nil { return nil, nil } - configMap := make(map[string]interface{}) - if err := entry.DecodeJSON(&configMap); err != nil { - return nil, err - } - var certificates []string - certificatesIfc := configMap["certificates"].([]interface{}) - for _, certificateIfc := range certificatesIfc { - certificates = append(certificates, certificateIfc.(string)) - } - config, err := models.NewConfiguration( - certificates, - configMap["pcf_api_addr"].(string), - configMap["pcf_username"].(string), - configMap["pcf_password"].(string), - ) - if err != nil { + config := &models.Configuration{} + if err := entry.DecodeJSON(config); err != nil { return nil, err } return config, nil diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-pcf/path_login.go b/vendor/github.com/hashicorp/vault-plugin-auth-pcf/path_login.go index 2353e75e03..752987b23c 100644 --- a/vendor/github.com/hashicorp/vault-plugin-auth-pcf/path_login.go +++ b/vendor/github.com/hashicorp/vault-plugin-auth-pcf/path_login.go @@ -3,17 +3,16 @@ package pcf import ( "context" "fmt" - "github.com/hashicorp/vault/sdk/helper/strutil" "net" "strings" "time" - "github.com/cloudfoundry-community/go-cfclient" "github.com/hashicorp/vault-plugin-auth-pcf/models" "github.com/hashicorp/vault-plugin-auth-pcf/signatures" "github.com/hashicorp/vault-plugin-auth-pcf/util" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/helper/cidrutil" + "github.com/hashicorp/vault/sdk/helper/strutil" "github.com/hashicorp/vault/sdk/logical" "github.com/pkg/errors" ) @@ -29,11 +28,11 @@ func (b *backend) pathLogin() *framework.Path { DisplayValue: "internally-defined-role", Description: "The name of the role to authenticate against.", }, - "certificate": { + "cf_instance_cert": { Required: true, Type: framework.TypeString, - DisplayName: "Client Certificate", - Description: "The full client certificate available at the CF_INSTANCE_CERT path on the PCF instance.", + DisplayName: "CF_INSTANCE_CERT Contents", + Description: "The full body of the file available at the CF_INSTANCE_CERT path on the PCF instance.", }, "signing_time": { Required: true, @@ -60,8 +59,8 @@ func (b *backend) pathLogin() *framework.Path { } // operationLoginUpdate is called by those wanting to gain access to Vault. -// They present a client certificate that should have been issued by the pre-configured -// Certificate Authority, and a signature that should have been signed by the client cert's +// They present the instance certificates that should have been issued by the pre-configured +// Certificate Authority, and a signature that should have been signed by the instance cert's // private key. If this holds true, there are additional checks verifying everything looks // good before authentication is given. func (b *backend) operationLoginUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { @@ -78,9 +77,9 @@ func (b *backend) operationLoginUpdate(ctx context.Context, req *logical.Request return logical.ErrorResponse("'signature' is required"), nil } - clientCertificate := data.Get("certificate").(string) - if clientCertificate == "" { - return logical.ErrorResponse("'certificate' is required"), nil + cfInstanceCertContents := data.Get("cf_instance_cert").(string) + if cfInstanceCertContents == "" { + return logical.ErrorResponse("'cf_instance_cert' is required"), nil } signingTimeRaw := data.Get("signing_time").(string) @@ -92,31 +91,6 @@ func (b *backend) operationLoginUpdate(ctx context.Context, req *logical.Request return logical.ErrorResponse(err.Error()), nil } - // Ensure the time it was signed is no more than 5 minutes in the past - // or 30 seconds in the future. This is a guard against some replay attacks. - fiveMinutesAgo := timeReceived.Add(time.Minute * time.Duration(-5)) - thirtySecondsFromNow := timeReceived.Add(time.Second * time.Duration(30)) - if signingTime.Before(fiveMinutesAgo) { - return logical.ErrorResponse(fmt.Sprintf("request is too old; signed at %s but received request at %s; raw signing time is %s", signingTime, timeReceived, signingTimeRaw)), nil - } - if signingTime.After(thirtySecondsFromNow) { - return logical.ErrorResponse(fmt.Sprintf("request is too far in the future; signed at %s but received request at %s; raw signing time is %s", signingTime, timeReceived, signingTimeRaw)), nil - } - - // Ensure the private key used to create the signature matches our client - // certificate, and that it signed the same data as is presented in the body. - // This offers some protection against MITM attacks. - matchingCert, err := signatures.Verify(signature, &signatures.SignatureData{ - SigningTime: signingTime, - Role: roleName, - Certificate: clientCertificate, - }) - if err != nil { - return logical.ErrorResponse(err.Error()), nil - } - - // Ensure the matching certificate was actually issued by the CA configured. - // This protects against self-generated client certificates. config, err := config(ctx, req.Storage) if err != nil { return nil, err @@ -124,20 +98,51 @@ func (b *backend) operationLoginUpdate(ctx context.Context, req *logical.Request if config == nil { return nil, errors.New("no CA is configured for verifying client certificates") } - verifyOpts, err := config.VerifyOpts() - if err != nil { - return nil, err + + // Ensure the time it was signed isn't too far in the past or future. + oldestAllowableSigningTime := timeReceived.Add(-1 * config.LoginMaxSecOld) + furthestFutureAllowableSigningTime := timeReceived.Add(config.LoginMaxSecAhead) + if signingTime.Before(oldestAllowableSigningTime) { + return logical.ErrorResponse(fmt.Sprintf("request is too old; signed at %s but received request at %s; allowable seconds old is %d", signingTime, timeReceived, config.LoginMaxSecOld/time.Second)), nil } - if _, err := matchingCert.Verify(verifyOpts); err != nil { + if signingTime.After(furthestFutureAllowableSigningTime) { + return logical.ErrorResponse(fmt.Sprintf("request is too far in the future; signed at %s but received request at %s; allowable seconds in the future is %d", signingTime, timeReceived, config.LoginMaxSecAhead/time.Second)), nil + } + + intermediateCert, identityCert, err := util.ExtractCertificates(cfInstanceCertContents) + if err != nil { + return logical.ErrorResponse(err.Error()), nil + } + + // Ensure the private key used to create the signature matches our identity + // certificate, and that it signed the same data as is presented in the body. + // This offers some protection against MITM attacks. + signingCert, err := signatures.Verify(signature, &signatures.SignatureData{ + SigningTime: signingTime, + Role: roleName, + CFInstanceCertContents: cfInstanceCertContents, + }) + if err != nil { + return logical.ErrorResponse(err.Error()), nil + } + // Make sure the identity/signing cert was actually issued by our CA. + if err := util.Validate(config.IdentityCACertificates, intermediateCert, identityCert, signingCert); err != nil { return logical.ErrorResponse(err.Error()), nil } // Read PCF's identity fields from the certificate. - pcfCert, err := models.NewPCFCertificateFromx509(matchingCert) + pcfCert, err := models.NewPCFCertificateFromx509(signingCert) if err != nil { return nil, err } + // It may help some users to be able to easily view the incoming certificate information + // in an un-encoded format, as opposed to the encoded format that will appear in the Vault + // audit logs. + if b.Logger().IsDebug() { + b.Logger().Debug(fmt.Sprintf("handling login attempt from %+v", pcfCert)) + } + // Ensure the pcf certificate meets the role's constraints. role, err := getRole(ctx, req.Storage, roleName) if err != nil { @@ -228,13 +233,7 @@ func (b *backend) pathLoginRenew(ctx context.Context, req *logical.Request, data } // Reconstruct the certificate and ensure it still meets all constraints. - pcfCert, err := models.NewPCFCertificate( - instanceID, - orgID, - spaceID, - appID, - ipAddr, - ) + pcfCert, err := models.NewPCFCertificate(instanceID, orgID, spaceID, appID, ipAddr) if err := b.validate(config, role, pcfCert, req.Connection.RemoteAddr); err != nil { return logical.ErrorResponse(err.Error()), nil } @@ -269,26 +268,13 @@ func (b *backend) validate(config *models.Configuration, role *models.RoleEntry, } // Use the PCF API to ensure everything still exists and to verify whatever we can. - client, err := cfclient.NewClient(&cfclient.Config{ - ApiAddress: config.PCFAPIAddr, - Username: config.PCFUsername, - Password: config.PCFPassword, - }) + client, err := util.NewPCFClient(config) if err != nil { return err } - // Check everything we can using the instance ID. - serviceInstance, err := client.GetServiceInstanceByGuid(pcfCert.InstanceID) - if err != nil { - return err - } - if serviceInstance.Guid != pcfCert.InstanceID { - return fmt.Errorf("cert instance ID %s doesn't match API's expected one of %s", pcfCert.InstanceID, serviceInstance.Guid) - } - if serviceInstance.SpaceGuid != pcfCert.SpaceID { - return fmt.Errorf("cert space ID %s doesn't match API's expected one of %s", pcfCert.SpaceID, serviceInstance.SpaceGuid) - } + // Here, if it were possible, we _would_ do an API call to check the instance ID, + // but currently there's no known way to do that via the pcf API. // Check everything we can using the app ID. app, err := client.AppByGuid(pcfCert.AppID) diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-pcf/signatures/version1.go b/vendor/github.com/hashicorp/vault-plugin-auth-pcf/signatures/version1.go index 8b733c1e52..be497d2367 100644 --- a/vendor/github.com/hashicorp/vault-plugin-auth-pcf/signatures/version1.go +++ b/vendor/github.com/hashicorp/vault-plugin-auth-pcf/signatures/version1.go @@ -21,7 +21,13 @@ const TimeFormat = "2006-01-02T15:04:05Z" type SignatureData struct { SigningTime time.Time Role string - Certificate string + + // CFInstanceCertContents are the full contents/body of the file + // available at CF_INSTANCE_CERT. When viewed visually, this file + // will contain two certificates. Generally, the first one is the + // identity certificate itself, and the second one is the intermediate + // certificate that issued it. + CFInstanceCertContents string } func (s *SignatureData) hash() []byte { @@ -31,7 +37,7 @@ func (s *SignatureData) hash() []byte { func (s *SignatureData) toSign() string { toHash := "" - for _, field := range []string{s.SigningTime.UTC().Format(TimeFormat), s.Certificate, s.Role} { + for _, field := range []string{s.SigningTime.UTC().Format(TimeFormat), s.CFInstanceCertContents, s.Role} { toHash += field } return toHash @@ -62,13 +68,11 @@ func Sign(pathToPrivateKey string, signatureData *SignatureData) (string, error) return base64.URLEncoding.EncodeToString(signatureBytes), nil } -// Verify ensures that a given signature was created by one of the private keys -// matching one of the given client certificates. It is possible for a client -// certificate string given by PCF to contain multiple certificates within its -// body, hence the looping. The matching certificate is returned and should be -// further checked to ensure it contains the app, space, and org ID, and CN; -// otherwise it would be possible to match against an injected client certificate -// to gain authentication. +// Verify ensures that a given signature was created by a private key +// matching one of the given instance certificates. It returns the matching +// certificate, which should further be verified to be the identity certificate, +// and to be issued by a chain leading to the root CA certificate. There's a +// util function for this named Validate. func Verify(signature string, signatureData *SignatureData) (*x509.Certificate, error) { if signatureData == nil { return nil, errors.New("signatureData must be provided") @@ -80,58 +84,35 @@ func Verify(signature string, signatureData *SignatureData) (*x509.Certificate, return nil, err } - certBytes := []byte(signatureData.Certificate) + cfInstanceCertContentsBytes := []byte(signatureData.CFInstanceCertContents) var block *pem.Block var result error for { - block, certBytes = pem.Decode(certBytes) + block, cfInstanceCertContentsBytes = pem.Decode(cfInstanceCertContentsBytes) if block == nil { break } - clientCerts, err := x509.ParseCertificates(block.Bytes) + instanceCerts, err := x509.ParseCertificates(block.Bytes) if err != nil { result = multierror.Append(result, err) continue } - for _, clientCert := range clientCerts { - publicKey, ok := clientCert.PublicKey.(*rsa.PublicKey) + for _, instanceCert := range instanceCerts { + publicKey, ok := instanceCert.PublicKey.(*rsa.PublicKey) if !ok { - result = multierror.Append(result, fmt.Errorf("not an rsa public key, it's a %t", clientCert.PublicKey)) + result = multierror.Append(result, fmt.Errorf("not an rsa public key, it's a %t", instanceCert.PublicKey)) continue } - if err := rsa.VerifyPSS(publicKey, crypto.SHA256, signatureData.hash(), signatureBytes, nil); err != nil { result = multierror.Append(result, err) continue } // Success - return clientCert, nil + return instanceCert, nil } } if result == nil { - return nil, fmt.Errorf("no matching client certificate found for %s in %s", signature, signatureData.Certificate) + return nil, fmt.Errorf("no matching certificate found for %s in %s", signature, signatureData.CFInstanceCertContents) } return nil, result } - -func IsIssuer(pathToCACert string, clientCert *x509.Certificate) (bool, error) { - caCertBytes, err := ioutil.ReadFile(pathToCACert) - if err != nil { - return false, err - } - - pool := x509.NewCertPool() - if ok := pool.AppendCertsFromPEM(caCertBytes); !ok { - return false, errors.New("couldn't append CA certificates") - } - - verifyOpts := x509.VerifyOptions{ - Roots: pool, - } - - if _, err := clientCert.Verify(verifyOpts); err != nil { - return false, err - } - // Success - return true, nil -} diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-pcf/testing/certificates/generate.go b/vendor/github.com/hashicorp/vault-plugin-auth-pcf/testing/certificates/generate.go index 08de2f80f0..08c1b8e0b8 100644 --- a/vendor/github.com/hashicorp/vault-plugin-auth-pcf/testing/certificates/generate.go +++ b/vendor/github.com/hashicorp/vault-plugin-auth-pcf/testing/certificates/generate.go @@ -106,10 +106,29 @@ func (e *TestCertificates) Close() error { } func generate(instanceID, orgID, spaceID, appID, ipAddress string) (caCert, instanceCert, instanceKey string, err error) { - caPrivateKey, err := rsa.GenerateKey(rand.Reader, 4096) + caCert, caPriv, err := generateCA("", nil) if err != nil { return "", "", "", err } + + intermediateCert, intermediatePriv, err := generateCA(caCert, caPriv) + if err != nil { + return "", "", "", err + } + + identityCert, identityPriv, err := generateIdentity(intermediateCert, intermediatePriv, instanceID, orgID, spaceID, appID, ipAddress) + if err != nil { + return "", "", "", err + } + + // Convert the identity key to something appropriate for a file body. + out := &bytes.Buffer{} + pem.Encode(out, pemBlockForKey(identityPriv)) + instanceKey = out.String() + return caCert, fmt.Sprintf("%s%s", intermediateCert, identityCert), instanceKey, nil +} + +func generateCA(caCert string, caPriv *rsa.PrivateKey) (string, *rsa.PrivateKey, error) { template := x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{ @@ -119,35 +138,66 @@ func generate(instanceID, orgID, spaceID, appID, ipAddress string) (caCert, inst CommonName: "test-CA", }, NotBefore: time.Now(), - NotAfter: time.Now().Add(time.Hour * 24 * 180), + NotAfter: time.Now().Add(time.Hour * 24 * 365 * 100), KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, IsCA: true, } - derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(caPrivateKey), caPrivateKey) - if err != nil { - return "", "", "", err + // Default to self-signing certificates by listing using itself as a parent. + parent := &template + + // If a cert is provided, use it as the parent. + if caCert != "" { + block, certBytes := pem.Decode([]byte(caCert)) + if block == nil { + return "", nil, errors.New("block shouldn't be nil") + } + if len(certBytes) > 0 { + return "", nil, errors.New("there shouldn't be more bytes") + } + ca509cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return "", nil, err + } + parent = ca509cert } + // If a private key isn't provided, make a new one. + priv := caPriv + if priv == nil { + newPriv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return "", nil, err + } + priv = newPriv + } + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, parent, publicKey(priv), priv) + if err != nil { + return "", nil, err + } + out := &bytes.Buffer{} pem.Encode(out, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) - caCert = out.String() - out.Reset() + cert := out.String() + return cert, priv, nil +} +func generateIdentity(caCert string, caPriv *rsa.PrivateKey, instanceID, orgID, spaceID, appID, ipAddress string) (string, *rsa.PrivateKey, error) { block, certBytes := pem.Decode([]byte(caCert)) if block == nil { - return "", "", "", errors.New("block shouldn't be nil") + return "", nil, errors.New("block shouldn't be nil") } if len(certBytes) > 0 { - return "", "", "", errors.New("there shouldn't be more bytes") + return "", nil, errors.New("there shouldn't be more bytes") } ca509cert, err := x509.ParseCertificate(block.Bytes) if err != nil { - return "", "", "", err + return "", nil, err } - template = x509.Certificate{ + template := x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{ Country: []string{"US"}, @@ -161,7 +211,7 @@ func generate(instanceID, orgID, spaceID, appID, ipAddress string) (caCert, inst CommonName: instanceID, }, NotBefore: time.Now(), - NotAfter: time.Now().Add(time.Hour * 24 * 180), + NotAfter: time.Now().Add(time.Hour * 24 * 365 * 100), KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, @@ -169,25 +219,20 @@ func generate(instanceID, orgID, spaceID, appID, ipAddress string) (caCert, inst IPAddresses: []net.IP{net.ParseIP(ipAddress)}, } - clientPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048) + priv, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { - return "", "", "", err + return "", nil, err } - derBytes, err = x509.CreateCertificate(rand.Reader, &template, ca509cert, publicKey(clientPrivateKey), caPrivateKey) + derBytes, err := x509.CreateCertificate(rand.Reader, &template, ca509cert, publicKey(priv), caPriv) if err != nil { - return "", "", "", err + return "", nil, err } + out := &bytes.Buffer{} pem.Encode(out, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) - instanceCert = out.String() - out.Reset() - - pem.Encode(out, pemBlockForKey(clientPrivateKey)) - instanceKey = out.String() - out.Reset() - - return caCert, instanceCert, instanceKey, nil + cert := out.String() + return cert, priv, nil } func makePathTo(certOrKey string) (string, error) { diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-pcf/util/certificates.go b/vendor/github.com/hashicorp/vault-plugin-auth-pcf/util/certificates.go new file mode 100644 index 0000000000..89e2eedff9 --- /dev/null +++ b/vendor/github.com/hashicorp/vault-plugin-auth-pcf/util/certificates.go @@ -0,0 +1,79 @@ +package util + +import ( + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "reflect" + + "github.com/hashicorp/go-multierror" +) + +// ExtractCertificates takes the contents of the file at CF_INSTANCE_CERT, which typically are +// comprised of two certificates. One is the identity certificate, and one is an intermediate +// CA certificate which is crucial in linking the identity cert back to the configured root +// certificate. It splits these two certificates apart, and identifies the certificate marked +// as a CA as the intermediate cert, and the one not marked as a CA as the identity certificate. +// It may error if the given file contents or certificates aren't as expected. +func ExtractCertificates(cfInstanceCertContents string) (intermediateCert, identityCert *x509.Certificate, err error) { + certPairBytes := []byte(cfInstanceCertContents) + numCerts := 0 + var block *pem.Block + var result error + for { + block, certPairBytes = pem.Decode(certPairBytes) + if block == nil { + break + } + certs, err := x509.ParseCertificates(block.Bytes) + if err != nil { + result = multierror.Append(result, err) + continue + } + for _, cert := range certs { + if cert.IsCA { + intermediateCert = cert + } else { + identityCert = cert + } + numCerts++ + } + } + if numCerts != 2 { + result = multierror.Append(fmt.Errorf("expected 2 certs but received %s", certPairBytes)) + } + if intermediateCert == nil { + result = multierror.Append(fmt.Errorf("no intermediate certificate found in %s", certPairBytes)) + } + if identityCert == nil { + result = multierror.Append(fmt.Errorf("no identity cert found in %s", certPairBytes)) + } + return intermediateCert, identityCert, result +} + +// Validate takes a group of trusted CA certificates, an intermediate certificate, an identity certificate, +// and a signing certificate, and makes sure they have the following properties: +// - The identity certificate is the same as the signing certificate +// - The identity certificate chains to at least one trusted CA +func Validate(caCerts []string, intermediateCert, identityCert, signingCert *x509.Certificate) error { + if !reflect.DeepEqual(identityCert, signingCert) { + return errors.New("signature not generated by identity cert") + } + roots := x509.NewCertPool() + for _, caCert := range caCerts { + if ok := roots.AppendCertsFromPEM([]byte(caCert)); !ok { + return errors.New("couldn't append root certificate") + } + } + intermediates := x509.NewCertPool() + intermediates.AddCert(intermediateCert) + verifyOpts := x509.VerifyOptions{ + Roots: roots, + Intermediates: intermediates, + } + if _, err := signingCert.Verify(verifyOpts); err != nil { + return err + } + return nil +} diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-pcf/util/util.go b/vendor/github.com/hashicorp/vault-plugin-auth-pcf/util/util.go index e05138e0cf..9f60c17214 100644 --- a/vendor/github.com/hashicorp/vault-plugin-auth-pcf/util/util.go +++ b/vendor/github.com/hashicorp/vault-plugin-auth-pcf/util/util.go @@ -1,3 +1,42 @@ package util +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "net/http" + + "github.com/cloudfoundry-community/go-cfclient" + "github.com/hashicorp/go-cleanhttp" + "github.com/hashicorp/vault-plugin-auth-pcf/models" +) + const BashTimeFormat = "Mon Jan 2 15:04:05 MST 2006" + +// NewPCFClient does some work that's needed every time we use the PCF client, +// namely using cleanhttp and configuring it to match the user conf. +func NewPCFClient(config *models.Configuration) (*cfclient.Client, error) { + clientConf := &cfclient.Config{ + ApiAddress: config.PCFAPIAddr, + Username: config.PCFUsername, + Password: config.PCFPassword, + HttpClient: cleanhttp.DefaultClient(), + } + rootCAs, err := x509.SystemCertPool() + if err != nil { + return nil, err + } + if rootCAs == nil { + rootCAs = x509.NewCertPool() + } + for _, certificate := range config.PCFAPICertificates { + if ok := rootCAs.AppendCertsFromPEM([]byte(certificate)); !ok { + return nil, fmt.Errorf("couldn't append PCF API cert to trust: %s", certificate) + } + } + tlsConfig := &tls.Config{ + RootCAs: rootCAs, + } + clientConf.HttpClient.Transport = &http.Transport{TLSClientConfig: tlsConfig} + return cfclient.NewClient(clientConf) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index baf10ea661..8c769e55de 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -335,7 +335,7 @@ github.com/hashicorp/vault-plugin-auth-gcp/plugin/cache github.com/hashicorp/vault-plugin-auth-jwt # github.com/hashicorp/vault-plugin-auth-kubernetes v0.5.1 github.com/hashicorp/vault-plugin-auth-kubernetes -# github.com/hashicorp/vault-plugin-auth-pcf v0.0.0-20190605234735-619218abcd26 +# github.com/hashicorp/vault-plugin-auth-pcf v0.0.0-20190619165123-fb996be2877c github.com/hashicorp/vault-plugin-auth-pcf github.com/hashicorp/vault-plugin-auth-pcf/signatures github.com/hashicorp/vault-plugin-auth-pcf/models