update to latest plugin dependencies

This commit is contained in:
Becca Petrin 2019-06-19 10:04:49 -07:00
parent 66aaa46588
commit 8bbf6e6fc3
18 changed files with 659 additions and 283 deletions

View File

@ -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
}

View File

@ -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)
}

2
go.mod
View File

@ -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

2
go.sum
View File

@ -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=

View File

@ -20,6 +20,3 @@ cmd/verify/verify
pkg*
bin*
# Ignore fake certificates generated for tests
testdata/fake-certificates*

View File

@ -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)

View File

@ -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`.

View File

@ -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 = `

View File

@ -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)

View File

@ -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

View File

@ -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"`
}

View File

@ -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 PCFs 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

View File

@ -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)

View File

@ -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
}

View File

@ -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) {

View File

@ -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
}

View File

@ -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)
}

2
vendor/modules.txt vendored
View File

@ -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