Skip to content

Commit

Permalink
Add GCP operator (#294)
Browse files Browse the repository at this point in the history
- Add GCP Operator to automatically create Vault static accounts for annotated k8s serviceAccounts
- Update VKCC GCP Sidecar to add ability to provide either GCP access tokens or service account keys based on the k8s serviceAccount annotations
  • Loading branch information
DTLP authored Oct 10, 2024
1 parent b77bbe1 commit 7535504
Show file tree
Hide file tree
Showing 30 changed files with 1,463 additions and 559 deletions.
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
FROM golang:1.21-alpine AS build
FROM golang:1.23-alpine AS build

WORKDIR /go/src/github.com/utilitywarehouse/vault-kube-cloud-credentials
COPY . /go/src/github.com/utilitywarehouse/vault-kube-cloud-credentials

ENV CGO_ENABLED 0
ENV CGO_ENABLED=0
RUN apk --no-cache add git \
&& go get -t ./... \
&& go test ./... \
Expand Down
95 changes: 74 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,57 @@ Vault Kube Cloud Credentials (lovingly VKCC - shorthand) - is an application
that runs in two modes - **operator** and **sidecar**.

As an **operator** - it watches for Kubernetes annotations in ServiceAccounts
and creates Vault objects - mapping that SA to the Cloud provider role value
inside the annotation.
and creates Vault objects - mapping that SA to the Cloud provider role or
service account value inside the annotation.

It uses a config file to define which namespaces are allowed to map to which
cloud provider roles.
cloud provider roles/service accounts.

Cloud providers supported:
- AWS

GCP - needs to be configured manually via a Terraform module:
[terraform/terraform-vault-gcp-binding](terraform/terraform-vault-gcp-binding)
- AWS
- GCP

As a **sidecar** - it runs next to you application container and exposes HTTP
endpoint that contains cloud provider credentials. Libraries such as AWS SDK
can consume such HTTP endpoint to always have up-to-date credentials.
In case if you would like to get GCP service account key file instead, the
sidecar will fetch the key and make the file available to your application
container.

Cloud providers supported:
- AWS
- GCP

- AWS
- GCP

## Operator

### Requirements

- A Vault server with:
* Kubernetes auth method, enabled and configured
* AWS secrets engine, enabled and configured
- Kubernetes auth method, enabled and configured
- AWS or GCP secrets engine, enabled and configured

### Usage

```
./vault-kube-cloud-credentials operator -provider={aws|gcp} [-config-file=PATH_TO_CONFIG_FILE]
```

Refer to the [example](manifests/operator/) for a reference Kubernetes
deployment.

Annotate your ServiceAccounts and the operator will create the corresponding
login role and AWS secret role in Vault at
login role and AWS secret role at
`auth/kubernetes/roles/<prefix>_aws_<namespace>_<name>` and
`aws/role/<prefix>_aws_<namespace>_<name>` respectively, where `<prefix>` is the
value of `prefix` in the configuration file (default: `vkcc`).
`aws/role/<prefix>_aws_<namespace>_<name>`
or, in case with GCP, will create a GCP static account at
`gcp/static-account/<prefix>_gcp_<namespace>_<name>` respectively, where
`<prefix>` is the value of `prefix` in the configuration file (default: `vkcc`).

```
AWS kube serviceAccount example:

```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
Expand All @@ -58,6 +69,28 @@ Valid Range for this value is [Minimum value of 900s, Maximum value of 43200s](h
this value should be lower then `maxLeaseTTL` of vault or `max_lease_ttl` on AWS backend config otherwise it will be capped at maxTTL.

_if custom TTL is set then make sure `max_session_duration` is updated in assume Role policy for the role if required, as it defaults to `1h`._

_GCP service account keys and access tokens have a default TTL of 1 hour._

GCP kube serviceAccount example:

```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: foobar
annotations:
vault.uw.systems/gcp-service-account: "foo@bar.iam.gserviceaccount.com"
vault.uw.systems/gcp-token-scopes: "https://www.googleapis.com/auth/cloud-platform"
```

If you specify `vault.uw.systems/gcp-token-scopes` annotation you will receive
GCP credentials in a form of access tokens. You can specify multiple scopes
separated by comma.
If that annotation is omitted the sidecar will fetch service account key in a
form of JSON file and save that to the path specified in `GOOGLE_APPLICATION_CREDENTIALS`
env var (`/gcp/sa.json` by default).

### Config file

The operator can be configured by a yaml file passed to the operator with the flag
Expand All @@ -70,24 +103,24 @@ Refer to the `defaultFileConfig` in [operator/config.go](operator/config.go).

#### Rules

You can control which service accounts can assume which roles based on their
namespace by setting rules under `aws.rules`.
You can control which service accounts can assume/use which roles based on their
namespace by setting rules under `aws.rules` and `gcp.rules`.

For example, the following configuration allows service accounts in `kube-system`
and namespaces prefixed with `system-` to assume roles under the `sysadmin/*` path,
roles that begin with `sysadmin-` or a specific `org/s3-admin` role in the accounts
`000000000000` and `111111111111`.

```
```yaml
aws:
rules:
- namespacePatterns:
- kube-system
- system-*
roleNamePatterns:
- sysadmin-*
- sysadmin/*
- org/s3-admin
- sysadmin-*
- sysadmin/*
- org/s3-admin
accountIDs:
- 000000000000
- 111111111111
Expand All @@ -96,6 +129,20 @@ aws:
If `accountIDs` is omitted or empty then any account is permitted. The other two
parameters are required.

The following GCP configuration allows service accounts in `kube-system` get
access to `foo@bar.iam.gserviceaccount.com` GCP service account and all accounts
that start with `baz` in `bar` project.

```yaml
gcp:
rules:
- namespacePatterns:
- kube-system
serviceAccountEmailPatterns:
- foo@bar.iam.gserviceaccount.com
- baz-*@bar.iam.gserviceaccount.com
```

The pattern matching supports [shell file name
patterns](https://golang.org/pkg/path/filepath/#Match).

Expand Down Expand Up @@ -134,7 +181,13 @@ Supported providers (secret engines):
- `gcp`

```
./vault-kube-cloud-credentials sidecar -vault-role=<prefix>_<provider>_<namespace>_<serviceaccount>
# AWS
./vault-kube-cloud-credentials sidecar \
-vault-role=<prefix>_<provider>_<namespace>_<serviceaccount>
# GCP
./vault-kube-cloud-credentials sidecar \
-vault-static-account=<prefix>_<provider>_<namespace>_<serviceaccount> \
-secret-type=access_token
```

Refer to the usage for more options:
Expand Down
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module github.com/utilitywarehouse/vault-kube-cloud-credentials

go 1.21
go 1.22.0

toolchain go1.22.5

require (
Expand Down Expand Up @@ -72,7 +73,7 @@ require (
golang.org/x/crypto v0.24.0 // indirect
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/oauth2 v0.22.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/term v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand Down
46 changes: 34 additions & 12 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@ import (
var (
operatorCommand = flag.NewFlagSet("operator", flag.ExitOnError)
flagOperatorConfigFile = operatorCommand.String("config-file", "", "Path to a configuration file")
flagOperatorProvider = operatorCommand.String("provider", "aws", "Cloud provider (one of 'aws' or 'gcp')")

sidecarCommand = flag.NewFlagSet("sidecar", flag.ExitOnError)
flagSidecarKubeTokenPath = sidecarCommand.String("kube-token-path", "/var/run/secrets/kubernetes.io/serviceaccount/token", "Path to the kubernetes serviceaccount token")
flagSidecarListenAddr = sidecarCommand.String("listen-address", "127.0.0.1:8098", "Listen address")
flagSidecarOpsAddr = sidecarCommand.String("operational-address", ":8099", "Listen address for operational status endpoints")
flagSidecarVaultRole = sidecarCommand.String("vault-role", "", "Must be in the format: `<prefix>_<provider>_<namespace>_<service-account>`")
sidecarCommand = flag.NewFlagSet("sidecar", flag.ExitOnError)
flagSidecarKubeTokenPath = sidecarCommand.String("kube-token-path", "/var/run/secrets/kubernetes.io/serviceaccount/token", "Path to the kubernetes serviceaccount token")
flagSidecarListenAddr = sidecarCommand.String("listen-address", "127.0.0.1:8098", "Listen address")
flagSidecarOpsAddr = sidecarCommand.String("operational-address", ":8099", "Listen address for operational status endpoints")
flagSidecarVaultRole = sidecarCommand.String("vault-role", "", "Must be in the format: `<prefix>_<provider>_<namespace>_<service-account>`")
flagSidecarVaultStaticAccount = sidecarCommand.String("vault-static-account", "", "Must be in the format: `<prefix>_<provider>_<namespace>_<service-account>`")
flagSidecarSecretType = sidecarCommand.String("secret-type", "access_token", "Secret type (one of 'service_account_key' or 'access_token')")

log = ctrl.Log.WithName("main")

Expand Down Expand Up @@ -75,7 +78,7 @@ func main() {
os.Exit(1)
}

o, err := operator.New(*flagOperatorConfigFile)
o, err := operator.New(*flagOperatorConfigFile, *flagOperatorProvider)
if err != nil {
log.Error(err, "error creating operator")
os.Exit(1)
Expand All @@ -95,28 +98,47 @@ func main() {
os.Exit(1)
}

var pc sidecar.ProviderConfig
provider := vaultRoleRegex.FindStringSubmatch(*flagSidecarVaultRole)[2]
var sidecarProvider string
if *flagSidecarVaultStaticAccount != "" && *flagSidecarVaultRole != "" {
log.Error(nil, "Only one of 'vault-role' or 'vault-static-account' can be specified.")
os.Exit(1)
}

if *flagSidecarVaultStaticAccount != "" {
sidecarProvider = vaultRoleRegex.FindStringSubmatch(*flagSidecarVaultStaticAccount)[2]
}
if *flagSidecarVaultRole != "" {
sidecarProvider = vaultRoleRegex.FindStringSubmatch(*flagSidecarVaultRole)[2]
}

switch provider {
var pc sidecar.ProviderConfig
switch sidecarProvider {
case "aws":
pc = &sidecar.AWSProviderConfig{
Path: "aws",
RoleArn: "",
Role: *flagSidecarVaultRole,
}
case "gcp":
keyFilePath := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")
if keyFilePath == "" {
keyFilePath = "/gcp/sa.json"
}

pc = &sidecar.GCPProviderConfig{
Path: "gcp",
RoleSet: *flagSidecarVaultRole,
Path: "gcp",
StaticAccount: *flagSidecarVaultStaticAccount,
SecretType: *flagSidecarSecretType,
KeyFileDestinationPath: keyFilePath,
}
default:
usage()
return
}

sidecarConfig := &sidecar.Config{
KubeAuthPath: "kubernetes",
KubeAuthRole: *flagSidecarVaultRole,
KubeAuthRole: *flagSidecarVaultStaticAccount,
ListenAddress: *flagSidecarListenAddr,
OpsAddress: *flagSidecarOpsAddr,
ProviderConfig: pc,
Expand Down
3 changes: 2 additions & 1 deletion manifests/examples/gcp-sidecar/gcp-probe.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ spec:
image: quay.io/utilitywarehouse/vault-kube-cloud-credentials
args:
- sidecar
- -vault-role=dev_gcp_$(POD_NAMESPACE)_$(POD_SERVICE_ACCOUNT)
- -vault-static-account=dev_gcp_$(POD_NAMESPACE)_$(POD_SERVICE_ACCOUNT)
- -secret-type=access_token
env:
- name: VAULT_ADDR
value: "https://vault.example-namespace:8200"
Expand Down
7 changes: 5 additions & 2 deletions manifests/examples/operator/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ bases:
resources:
- rbac.yaml
configMapGenerator:
- name: vault-kube-cloud-credentials-operator
- name: vault-kube-cloud-credentials-operator-aws
files:
- config.yaml=resources/operator-config.yaml
- config.yaml=resources/operator-aws-config.yaml
- name: vault-kube-cloud-credentials-operator-gcp
files:
- config.yaml=resources/operator-gcp-config.yaml
secretGenerator:
- name: vault
envs:
Expand Down
20 changes: 17 additions & 3 deletions manifests/examples/operator/rbac.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: vault-kube-cloud-credentials-operator
name: vault-kube-cloud-credentials-operator-aws
roleRef:
kind: ClusterRole
name: vault-kube-cloud-credentials-operator
name: vault-kube-cloud-credentials-operator-aws
apiGroup: rbac.authorization.k8s.io
subjects:
- kind: ServiceAccount
name: vault-kube-cloud-credentials-operator
name: vault-kube-cloud-credentials-operator-aws
# update with the namespace where the operator is running
namespace: example
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: vault-kube-cloud-credentials-operator-gcp
roleRef:
kind: ClusterRole
name: vault-kube-cloud-credentials-operator-gcp
apiGroup: rbac.authorization.k8s.io
subjects:
- kind: ServiceAccount
name: vault-kube-cloud-credentials-operator-gcp
# update with the namespace where the operator is running
namespace: example
11 changes: 11 additions & 0 deletions manifests/examples/operator/resources/operator-gcp-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
prefix: "dev"
kubernetesAuthBackend: kubernetes
gcp:
path: gcp
rules:
- namespacePatterns:
- kube-system
- system-*
serviceAccountEmailPatterns:
- foo@bar.iam.gserviceaccount.com
- baz-*@bar.iam.gserviceaccount.com
Loading

0 comments on commit 7535504

Please sign in to comment.