Vault plugin that exchanges a VAULT_TOKEN
for a GCP access_token
that as attenuated permissions.
The existing GCP access_token
secrets plugin plugin Vault provides uses the credentials Vault is seeded with to create additional serviceAccounts “per roleset” and then assign them IAM permissions on GCP resources. Essentially, its creating N service account and then applying permissions. This technique has several limitations which is described in the docs.
In contrast, this plugin uses Vaults Service Account to perform IAM Impersonation on another Service Account and get its access_token
. From there, the access_token
that represents the target service account can be restricted further by applying Credential Access Boundary rules.
In other words, this plugin does not create new service accounts but rather assumes the identity of another service account and then attenuates the scope of GCP resources that new token can access.
NOTE: since this plugin uses service account impersonation, the target account(s) to impersonate must already exist.
You can find the source here
There are two modes of operation:
Restricted Token
In this mode, the VAULT admin defines a policy that stipulates the specific service account a client can assume and the CAB resources it apply to.
A VAULT_TOKEN
bearer cannot request extensions to include resources beyond what the admin defined for the CAB
Unrestricted Token
In this mode, the VAULT admin defines a policy that allows the VAULT_TOKEN
bearer to get an access_token
that represents the impersonated account but the token is not attenuated.
This mode is essentially impersonation except that the vault admin can define basic settings like lifetime, delegation and scopes for the impersonated token.
the difference between 1 and 2 is that the admin defines the CAB restricts but in 2, the client requests the CAB restrict for a predefined service account that the admin assigned by policy.
Impersonation
In this mode, the VAULT admin defines a policy that allows the VAULT_TOKEN
bearer to get an access_token
that represents the impersonated account but the token is not attenuated.
This mode is service account impersonation without scope or lifetime overrides an admin may have set as would be the case with an unrestricted token
Downscoped tokens only work with certain services like GCS
NOTE: this repository and plugin is NOT supported by Google
Basically, the code uses Vaults Credentials to impersonate a service account and then downscope
import (
"golang.org/x/oauth2/google/downscope"
"google.golang.org/api/impersonate"
"google.golang.org/api/option"
)
targetPrincipal := "impersonated-account@project.iam.gserviceaccount.com"
lifetime := 30 * time.Second
delegates := []string{}
targetScopes := []string{"https://www.googleapis.com/auth/cloud-platform"}
creds, _ := google.FindDefaultCredentials(ctx, cloudPlatformScope)
// first impersonate the target service account
impersonatedTokenSource, _err_ := impersonate.CredentialsTokenSource(ctx, impersonate.CredentialsConfig{
TargetPrincipal: targetPrincipal,
Scopes: targetScopes,
Lifetime: lifetime,
Delegates: delegates,
}, option.WithCredentials(creds))
// then downscope that impersonated account
accessBoundary := []downscope.AccessBoundaryRule{
{
AvailableResource: "//storage.googleapis.com/projects/_/buckets/" + bucketName,
AvailablePermissions: []string{"inRole:roles/storage.objectViewer"},
Condition: &downscope.AvailabilityCondition{
Expression: expression,
},
},
}
downScopedTokenSource, _ := downscope.NewTokenSource(ctx, downscope.DownscopingConfig{RootSource: impersonatedTokenSource, Rules: accessBoundary})
// return token from the downScopedTokenSource()
The following quick start uses Vault in dev
mode. You’ll need
golang1.16+
make
Vault v1.8.1
First configure the two service accounts.
vault-server
: This is the service account vault runs asimpersonated-account
: This is the service account vault can impersonateAllow vault-server
permission to impersonate impersonated-account
Create two GCS Buckets and allow generic-server
permissions on both.
Allow vault’s service account to impersonate IMPERSONATED_SERVICE_ACCOUNT
export PROJECT_ID=`gcloud config get-value core/project`
export PROJECT_NUMBER=`gcloud projects describe $PROJECT_ID --format="value(projectNumber)"`
export BUCKET_1=$PROJECT_ID-cab1
export BUCKET_2=$PROJECT_ID-cab2
gcloud iam service-accounts create vault-server --display-name "Vault Root Service Account"
gcloud iam service-accounts keys create svc_account.json --iam-account=vault-server@$PROJECT_ID.iam.gserviceaccount.com
export VAULT_SERVICE_ACCOUNT=vault-server@$PROJECT_ID.iam.gserviceaccount.com
gcloud iam service-accounts create impersonated-account --display-name "Impersonated Service Account"
export IMPERSONATED_SERVICE_ACCOUNT=impersonated-account@$PROJECT_ID.iam.gserviceaccount.com
echo $VAULT_SERVICE_ACCOUNT
echo $IMPERSONATED_SERVICE_ACCOUNT
echo $BUCKET_1
echo $BUCKET_2
gsutil mb gs://$BUCKET_1
gsutil mb gs://$BUCKET_2
echo foo1 > file1.txt
echo foo2 > file2.txt
gsutil cp file1.txt gs://$BUCKET_1/
gsutil cp file2.txt gs://$BUCKET_2/
gsutil uniformbucketlevelaccess set on gs://$BUCKET_1
gsutil uniformbucketlevelaccess set on gs://$BUCKET_2
gsutil iam ch serviceAccount:$IMPERSONATED_SERVICE_ACCOUNT:objectCreator,objectViewer gs://$BUCKET_1
gsutil iam ch serviceAccount:$IMPERSONATED_SERVICE_ACCOUNT:objectViewer gs://$BUCKET_2
# allow Vault service account to impersonate
gcloud iam service-accounts add-iam-policy-binding $IMPERSONATED_SERVICE_ACCOUNT \
--member=serviceAccount:$VAULT_SERVICE_ACCOUNT --role=roles/iam.serviceAccountTokenCreator
The iam.serviceAccountTokenCreator
role includes concentric set of permission (see Concentric IAMCredentials Permissions: The secret life of signBlob. If you just need to generate an access_token
or an id_token
, consider generating a custom role with that specific capability.
Configure the .hcl
and CAB configuration files to use
cab.json.tmpl
defines a rule that allows only access to one of the two buckets
cab_override.json.tmpl
defines a rule that allows access to both buckets
envsubst < "cab.json.tmpl" > "cab.json"
envsubst < "cab_override.json.tmpl" > "cab_override.json"
envsubst < "tokenpolicy.hcl.tmpl" > "tokenpolicy.hcl"
export VAULT_ADDR='http://localhost:8200'
export GOBIN=`pwd`/bin
rm bin/vault-plugin-secrets-gcp-cab
make fmt
make dev
export GOOGLE_APPLICATION_CREDENTIALS=`pwd`/svc_account.json
vault server -dev -dev-plugin-dir=./bin --log-level=debug
At this point, Vault is using the service account defined in GOOGLE_APPLICATION_CREDENTIALS
In a new window, enable the plugin
For vault in -dev
mode:
export VAULT_ADDR='http://localhost:8200'
# set the VAULT_TOKEN if -dev switch is not used for vault server
# export VAULT_TOKEN=
export SHASUM=$(shasum -a 256 "bin/vault-plugin-secrets-gcp-cab" | cut -d " " -f1)
vault plugin register \
-sha256="${SHASUM}" \
-command="vault-plugin-secrets-gcp-cab" \
secret gcpcab
vault secrets enable -path="gcpcab" --plugin-name='vault-plugin-secrets-gcp-cab' plugin
Then configure the policy you are interested in. Make sure the the environment variables are set.
The following will configure both a restricted and unrestricted policy:
# create restricted token
vault write gcpcab/cab/config/myconfig \
project="$PROJECT_ID" \
target_service_account="$IMPERSONATED_SERVICE_ACCOUNT" \
scopes="https://www.googleapis.com/auth/cloud-platform" \
restricted=true \
duration="3000" \
bindings=@cab.json
vault policy write restricted-policy -<<EOF
path "gcpcab/cab/certname12020" {
capabilities = ["update", "delete"]
allowed_parameters = {
"config" = ["myconfig"]
}
}
EOF
vault token create -policy=restricted-policy
## now create an unrestricted token
vault write gcpcab/cab/config/myunrestrictedconfig \
project="$PROJECT_ID" \
target_service_account="$IMPERSONATED_SERVICE_ACCOUNT" \
scopes="https://www.googleapis.com/auth/cloud-platform" \
duration="3000" \
restricted=false
vault policy write unrestricted-policy -<<EOF
path "gcpcab/cab/certname22020" {
capabilities = ["update", "delete"]
allowed_parameters = {
"config" = ["myunrestrictedconfig"]
"duration" = ["3000"]
"bindings" = []
}
}
EOF
vault token create -policy=unrestricted-policy
In a new window, export the VAULT_ADDR
and VAULT_TOKEN
that was associated with the restricted policy:
export VAULT_ADDR='http://localhost:8200'
export VAULT_TOKEN=<yourtoken>
export PROJECT_ID=`gcloud config get-value core/project`
export PROJECT_NUMBER=`gcloud projects describe $PROJECT_ID --format="value(projectNumber)"`
export IMPERSONATED_SERVICE_ACCOUNT=impersonated-account@$PROJECT_ID.iam.gserviceaccount.com
export BUCKET_1=$PROJECT_ID-cab1
export BUCKET_2=$PROJECT_ID-cab2
And attempt to get the new access token:
vault write gcpcab/cab/certname12020 config="myconfig"
Key Value
--- -----
access_token ya29.dr....
Now use that token to access GCS buckets:
export TOKEN=ya29.dr....
curl -s -H "Authorization: Bearer $TOKEN" -o /dev/null -w "%{http_code}\n" https://storage.googleapis.com/storage/v1/b/$BUCKET_1/o/file1.txt
curl -s -H "Authorization: Bearer $TOKEN" -o /dev/null -w "%{http_code}\n" https://storage.googleapis.com/storage/v1/b/$BUCKET_2/o/file2.txt
200
403
The CAB rule cab.json
we defined allowed only access to $BUCKET_1
so that explains the latter 403
The unrestricted policy was originally set with reestricted=false
flag which means a VAULT_TOKEN
that uses it
can define the CAB rule but not the service account being impersonated
Use the corresponding CAB definition to do the override: cab_override.json
which allows read access to both buckets
{
"accessBoundaryRules" : [
{
"availableResource" : "//storage.googleapis.com/projects/_/buckets/$BUCKET_1",
"availablePermissions": ["inRole:roles/storage.objectViewer"],
"availabilityCondition" : {
"title" : "obj-exact",
"expression" : "resource.name == \"projects/_/buckets/$BUCKET_1/objects/file1.txt\""
}
},
{
"availableResource" : "//storage.googleapis.com/projects/_/buckets/$BUCKET_2",
"availablePermissions": ["inRole:roles/storage.objectViewer"],
"availabilityCondition" : {
"title" : "obj-exact",
"expression" : "resource.name == \"projects/_/buckets/$BUCKET_2/objects/file2.txt\""
}
}
]
}
So lets try to use that
vault write gcpcab/cab/certname22020 config="myunrestrictedconfig" \
duration="3000" bindings=@cab_override.json
Key Value
--- -----
access_token ya29.dr.ATVe...
curl -s -H "Authorization: Bearer $TOKEN" -o /dev/null -w "%{http_code}\n" https://storage.googleapis.com/storage/v1/b/$BUCKET_1/o/file1.txt
curl -s -H "Authorization: Bearer $TOKEN" -o /dev/null -w "%{http_code}\n" https://storage.googleapis.com/storage/v1/b/$BUCKET_2/o/file2.txt
200
200
Since the derived CAB config the user set allows for both bucket read, you’ll see 200
responses
You can also optionally request a completely unrestricted token. That is, you can apply a policy to return the non attenuated token for IMPERSONATED_SERVICE_ACCOUNT
.
To do this, specify restricted=false
and raw_token=true
in the policy config:
vault write gcpcab/cab/config/mytotallyunrestrictedconfig \
project="$PROJECT_ID" \
target_service_account="$IMPERSONATED_SERVICE_ACCOUNT" \
scopes="https://www.googleapis.com/auth/cloud-platform" \
restricted=false \
raw_token=true
vault policy write impersonated-policy -<<EOF
path "gcpcab/cab/certname62020" {
capabilities = ["update", "delete"]
allowed_parameters = {
"config" = ["mytotallyunrestrictedconfig"]
}
}
EOF
vault token create -policy=impersonated-policy
vault write gcpcab/cab/certname62020 config="mytotallyunrestrictedconfig"
If your Vault is running in non-dev mode and you uses our own Certs for TLS in server.conf
on host vault.domain.com
:
server.conf
backend "file" {
path = "/path/to/filebackend"
}
ui = true
listener "tcp" {
address = "vault.domain.com:8200"
tls_cert_file = "/path/to/crt_vault.pem"
tls_key_file = "/path/to/key_vault.pem"
}
api_addr = "https://vault.domain.com:8200"
plugin_directory = "/path/to/plugins"
(you can find the sample certs here). If you wan to test this locally, edit /etc/hosts
and set 127.0.0.1 vault.domain.com
vault started with
vault server --config server.conf
compile and install plugin
cd vault-plugin-secrets-gcp-cab
mkdir -p bin
export GOBIN=`pwd`/bin
rm bin/vault-plugin-secrets-gcp-cab
make fmt
make dev
export SHASUM=$(shasum -a 256 "bin/vault-plugin-secrets-gcp-cab" | cut -d " " -f1)
echo $SHASUM
cp bin/vault-plugin-secrets-gcp-cab /apps/vault/plugins
# assume this is the root token
export VAULT_TOKEN=...
export VAULT_SERVER=https://vault.domain.com:8200
export VAULT_CACERT=/apps/vault/CA_crt.pem
vault plugin register \
-sha256="${SHASUM}" \
-command="vault-plugin-secrets-gcp-cab" \
-args="-ca-cert=$VAULT_CACERT" gcpcab
vault secrets enable -path="gcpcab" --plugin-name='gcpcab' plugin
By default, the plugin will use Application Default Credentials
to access GCP CA. If you need different credentials per mount, you can specify that using the config/
path of the mount point. For example, to specify a credential file JSON certificate for the /gcpcab
mount:
vault write gcpcab/config \
credentials=@/path/to/svc_account.json
This site supports webmentions. Send me a mention via this form.