Skip to content

Commit

Permalink
Merge pull request #414 from micnncim/workload-identity
Browse files Browse the repository at this point in the history
Support Workload Identity
  • Loading branch information
turkenh authored Feb 13, 2022
2 parents f478213 + bd61620 commit 27f804f
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 6 deletions.
2 changes: 1 addition & 1 deletion apis/v1beta1/providerconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ type ProviderConfigSpec struct {
// ProviderCredentials required to authenticate.
type ProviderCredentials struct {
// Source of the provider credentials.
// +kubebuilder:validation:Enum=None;Secret;Environment;Filesystem
// +kubebuilder:validation:Enum=None;Secret;InjectedIdentity;Environment;Filesystem
Source xpv1.CredentialsSource `json:"source"`

xpv1.CommonCredentialSelectors `json:",inline"`
Expand Down
180 changes: 180 additions & 0 deletions docs/AUTHENTICATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# Authenticating to Google Cloud APIs

`provider-gcp` requires credentials to be provided in order to authenticate to
the Google Cloud APIs. This can be done in one of the following ways:

- Authenticating using a base-64 encoded service account key in a Kubernetes
`Secret`. This is described in detail [here](https://crossplane.io/docs/v1.6/getting-started/install-configure.html#get-gcp-account-keyfile).
- Authenticating using [Workload Identity](https://cloud.google.com/kubernetes-engine/docs/concepts/workload-identity).
This is described in the [section below](#authenticating-with-workload-identity).

## Authenticating with Workload Identity

*Note: This method is supported in `provider-gcp` v0.20.0 and later.*

Using Workload Identity requires some additional setup.
Many of the steps can also be found in the [documentation](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity).

### Steps

These steps assume you already have a running GKE cluster which has already
enabled Workload Identity and has a sufficiently large node pool.

#### 0. Prepare your variables

In the following sections, you'll need to name your resources.
Define the variables below with any names valid in Kubernetes or GCP so that you
can smoothly set it up:

```console
$ PROJECT_ID=<YOUR_GCP_PROJECT_ID> # e.g.) acme-prod
$ PROVIDER_GCP=<YOUR_PROVIDER_GCP_NAME> # e.g.) provider-gcp
$ VERSION=<YOUR_PROVIDER_GCP_VERSION> # e.g.) 0.20.0
$ GCP_SERVICE_ACCOUNT=<YOUR_CROSSPLANE_GCP_SERVICE_ACCOUNT_NAME> # e.g.) crossplane
$ ROLE=<YOUR_ROLE_FOR_CROSSPLANE_GCP_SERVICE_ACCOUNT> # e.g.) roles/cloudsql.admin
$ CONTROLLER_CONFIG=<YOUR_CONTROLLER_CONFIG_NAME> # e.g.) gcp-config (Optional)
```

#### 1. Install Crossplane

Install Crossplane from `stable` channel:

```bash
$ helm repo add crossplane-stable https://charts.crossplane.io/stable
$ helm install crossplane --create-namespace --namespace crossplane-system crossplane-stable/crossplane
```

`provider-gcp` can be installed with either the [Crossplane CLI](https://crossplane.io/docs/v1.6/getting-started/install-configure.html#install-crossplane-cli)
or a `Provider` resource as below:

```console
$ cat <<EOF | kubectl apply -f -
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: ${PROVIDER_GCP}
spec:
package: crossplane/provider-gcp:v${VERSION} # v0.20.0 or later
# Set a controllerConfigRef if you have a ControllerConfig:
# controllerConfigRef:
# name: ${CONTROLLER_CONFIG}
EOF
```

#### 2. Configure service accounts to use Workload Identity

Create a GCP service account, which will be used for provisioning actual
infrastructure in GCP, and grant IAM roles you need for accessing the Google
Cloud APIs:

```console
$ gcloud iam service-accounts create ${GCP_SERVICE_ACCOUNT}
$ gcloud projects add-iam-policy-binding ${PROJECT_ID} \
--member "serviceAccount:${GCP_SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com" \
--role ${ROLE} \
--project ${PROJECT_ID}
```

Get the name of your current `ProviderRevision` of this provider:

```console
$ REVISION=$(kubectl get providers.pkg.crossplane.io ${PROVIDER_GCP} -o jsonpath="{.status.currentRevision}")
```

Next, you'll configure IAM to use Workload Identity.
In this step, you can choose one of the following options to configure service accounts:

- [Option 1] Use a Kubernetes `ServiceAccount` managed by a provider's controller.
- [Option 2] Use a Kubernetes `ServiceAccount` which you created and is specified to `.spec.serviceAccountName`
in a [`ControllerConfig`](https://doc.crds.dev/github.com/crossplane/crossplane/pkg.crossplane.io/ControllerConfig/v1alpha1@v1.6.2).

##### 2.1. [Option 1] Use a controller-managed `ServiceAccount`

Specify a Kubernetes `ServiceAccount` with the revision you got in the last
step:

```console
$ KUBERNETES_SERVICE_ACCOUNT=${REVISION}
```

##### 2.1. [Option 2] Use a user-managed `ServiceAccount`

Name your Kubernetes `ServiceAccount`:

```console
$ KUBERNETES_SERVICE_ACCOUNT=<YOUR_KUBERNETES_SERVICE_ACCOUNT>
```

Create a `ServiceAccount`, `ControllerConfig`, and `ClusterRoleBinding`:

```console
$ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
name: ${KUBERNETES_SERVICE_ACCOUNT}
namespace: crossplane-system
---
apiVersion: pkg.crossplane.io/v1alpha1
kind: ControllerConfig
metadata:
name: ${CONTROLLER_CONFIG}
spec:
serviceAccountName: ${KUBERNETES_SERVICE_ACCOUNT}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: crossplane:provider:${PROVIDER_GCP}:system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: crossplane:provider:${REVISION}:system
subjects:
- kind: ServiceAccount
name: ${KUBERNETES_SERVICE_ACCOUNT}
namespace: crossplane-system
EOF
```

#### 2.2. Allow the Kubernetes `ServiceAccount` to impersonate the GCP service account

Grant `roles/iam.workloadIdentityUser` to the GCP service account:

```console
$ gcloud iam service-accounts add-iam-policy-binding \
${GCP_SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com \
--role roles/iam.workloadIdentityUser \
--member "serviceAccount:${PROJECT_ID}.svc.id.goog[crossplane-system/${KUBERNETES_SERVICE_ACCOUNT}]" \
--project ${PROJECT_ID}
```

Annotate the `ServiceAccount` with the email address of the GCP service account:

```console
$ kubectl annotate serviceaccount ${KUBERNETES_SERVICE_ACCOUNT} \
iam.gke.io/gcp-service-account=${GCP_SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com \
-n crossplane-system
```

### 3. Configure a `ProviderConfig`

Create a `ProviderConfig` with `InjectedIdentity` in `.spec.credentials.source`:

```console
$ cat <<EOF | kubectl apply -f -
apiVersion: gcp.crossplane.io/v1beta1
kind: ProviderConfig
metadata:
name: default
spec:
projectID: ${PROJECT_ID}
credentials:
source: InjectedIdentity
EOF
```

### 4. Next steps

Now that you have configured `provider-gcp` with Workload Identity supported,
you can [provision infrastructure](https://crossplane.io/docs/v1.6/getting-started/provision-infrastructure).
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/imdario/mergo v0.3.12
github.com/mitchellh/copystructure v1.0.0
github.com/pkg/errors v0.9.1
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
google.golang.org/api v0.52.0
google.golang.org/grpc v1.39.0
gopkg.in/alecthomas/kingpin.v2 v2.2.6
Expand Down
3 changes: 2 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -929,8 +929,9 @@ golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914 h1:3B43BWw0xEBsLZ/NO1VALz6fppU3481pik+2Ksv45z8=
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand Down
1 change: 1 addition & 0 deletions package/crds/gcp.crossplane.io_providerconfigs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ spec:
enum:
- None
- Secret
- InjectedIdentity
- Environment
- Filesystem
type: string
Expand Down
21 changes: 17 additions & 4 deletions pkg/clients/gcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"strings"

"github.com/google/go-cmp/cmp"
"golang.org/x/oauth2/google"
"google.golang.org/api/googleapi"
"google.golang.org/api/option"
"google.golang.org/grpc/codes"
Expand All @@ -31,6 +32,7 @@ import (
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"

xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
"github.com/crossplane/crossplane-runtime/pkg/errors"
"github.com/crossplane/crossplane-runtime/pkg/resource"

Expand All @@ -39,6 +41,8 @@ import (
"github.com/crossplane/provider-gcp/apis/v1beta1"
)

const scopeCloudPlatform = "https://www.googleapis.com/auth/cloud-platform"

// GetAuthInfo returns the necessary authentication information that is necessary
// to use when the controller connects to GCP API in order to reconcile the managed
// resource.
Expand Down Expand Up @@ -79,11 +83,20 @@ func UseProviderConfig(ctx context.Context, c client.Client, mg resource.Managed
if err := c.Get(ctx, types.NamespacedName{Name: mg.GetProviderConfigReference().Name}, pc); err != nil {
return "", nil, err
}
data, err := resource.CommonCredentialExtractor(ctx, pc.Spec.Credentials.Source, c, pc.Spec.Credentials.CommonCredentialSelectors)
if err != nil {
return "", nil, errors.Wrap(err, "cannot get credentials")
switch s := pc.Spec.Credentials.Source; s { //nolint:exhaustive
case xpv1.CredentialsSourceInjectedIdentity:
ts, err := google.DefaultTokenSource(ctx, scopeCloudPlatform)
if err != nil {
return "", nil, errors.Wrap(err, "cannot get application default credentials token")
}
return pc.Spec.ProjectID, option.WithTokenSource(ts), nil
default:
data, err := resource.CommonCredentialExtractor(ctx, pc.Spec.Credentials.Source, c, pc.Spec.Credentials.CommonCredentialSelectors)
if err != nil {
return "", nil, errors.Wrap(err, "cannot get credentials")
}
return pc.Spec.ProjectID, option.WithCredentialsJSON(data), nil
}
return pc.Spec.ProjectID, option.WithCredentialsJSON(data), nil
}

// IsErrorNotFoundGRPC gets a value indicating whether the given error represents
Expand Down

0 comments on commit 27f804f

Please sign in to comment.