diff --git a/README.md b/README.md index 3b5b5b7..f52673e 100644 --- a/README.md +++ b/README.md @@ -3,88 +3,52 @@ This repo contains the GCP plugin for [HashiCorp Boundary](https://www.boundaryproject.io/). -> [!IMPORTANT] -> This is a prototype. Not officially supported by HashiCorp. - ## Credentials -This plugin uses [Application Default Credentials (ADC)](https://cloud.google.com/docs/authentication/provide-credentials-adc) -to authenticate to Google and retrieve a list of instances and instance groups. +### Service Account Credentials -As a result, it does not support rotation of the credentials since they are inferred -from the environment. +The plugin will authenticate using service account credential when the `secrets` +object is set. By default, the plugin will attempt to rotate the credentials. +The given credentials will be used to create a new credential, and then the given +credential will be revoked. In this way, after rotation, only Boundary knows the +client secret in use by this plugin. + +Credential rotation can be turned off by setting the `disable_credential_rotation` +attribute to true. + +### Service Account Impersonation + +The plugin will attempt to impersonate a service account when the `target_service_account_id` +field is supplied through the `attributes` object. The base service account will be +used to assume the identity and permissions of the target service account. A temporary +credential will be generated for authentication. The base service account requires the +[service account token creator](https://cloud.google.com/iam/docs/service-account-permissions#token-creator-role) +role to assume the role of the target service account. + +By default, the credentials of the base service account will be rotated if +credential rotation is not disabled by setting the `disable_credential_rotation` +attribute. + +### Application Default Credentials + +The plugin uses [Application Default Credentials (ADC)](https://cloud.google.com/docs/authentication/provide-credentials-adc) +for authentication when no `secrets` object is set. The plugin will attempt to +retrieve the credentials based on the environment. ## Dynamic Hosts -This plugin supports dynamically sourcing instances from Google Compute Engine. +This plugin supports dynamically sourcing instances from GCP Google Compute Engine. -Host sets created with this plugin define filters or instance groups -which select and group like instances within Google; these host sets can in turn be +Host sets created with this plugin define filters +which select and group like instances within GCP; these host sets can in turn be added to targets within Boundary as host sources. At creation, update or deletion of a host catalog of this type, configuration of the plugin is performed via the attribute/secret values passed to the create, update, or delete calls actions. The values passed in to the plugin here are the attributes set -on on a host catalog in Boundary. +on a host catalog in Boundary. -### Google IAM permissions +The plugin fetches hosts through the [Instances.List](https://cloud.google.com/compute/docs/reference/rest/v1/instances/list#filter) +call. -The plugin requires the following permissions: - -- `compute.instances.get` -- `compute.instances.list` -- `compute.instanceGroups.get` -- `compute.instanceGroups.list` - -### Attributes - -### Host Catalog - -The following attributes are valid on a Google host catalog resource: - -- `project` (string): required. Project ID of the instances you want to add to host catalog. -- `zone` (string): required. Zone of the instances you want to add to host catalog. - -Example: - -```shell -$ boundary host-catalogs create plugin -scope-id p_1234567890 -plugin-name google -attr zone=us-central1-a -attr project=$GOOGLE_PROJECT -``` - -### Host Set - -The following attributes are valid on a Google host set resource: - -- `filter` (string): Google Cloud [filter expression](https://cloud.google.com/sdk/gcloud/reference/topic/filters) - to filter instances. - -- `instance_group` (string): Name of instance group to get a list of instances. - -You can only set a `filter` or `instance_group` attribute, you cannot set both. - -Example: - -```shell -$ boundary host-sets create plugin -host-catalog-id $HOST_CATALOG_ID -name "filter-example" -description "example using filters" -attr filter="status=RUNNING" - -$ boundary host-sets create plugin -host-catalog-id $HOST_CATALOG_ID -name "group-example" -description "example using instance groups" -attr instance_group="instance-group-name" -``` - -After generating the host set, create a target. - -```shell -$ boundary targets create tcp -name google -description "Google Cloud" -scope-id p_1234567890 -default-port 22 -$ boundary targets add-host-sources -id $TARGET_ID -host-source $HOST_SET_ID -``` - -## Setup - -You will need to build your own Boundary binary, as Boundary imports the plugins as a dependency. - -As a reference, you can review [this fork](https://github.com/joatmon08/boundary) for changes. - -To build the fork, go into the repository. - -1. Run `make build`. -1. Authenticate to Google Cloud using `gcloud auth application-default login`. -1. Run Boundary with `./bin/boundary dev`. \ No newline at end of file +[Getting Started](plugin/service/host/README.md) \ No newline at end of file diff --git a/go.mod b/go.mod index a692388..e622134 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/google/go-cmp v0.6.0 github.com/googleapis/gax-go/v2 v2.13.0 github.com/hashicorp/boundary/sdk v0.0.47 - github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 golang.org/x/oauth2 v0.22.0 google.golang.org/api v0.196.0 ) @@ -25,9 +24,6 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/s2a-go v0.1.8 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.3 // indirect - github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect - github.com/hashicorp/go-sockaddr v1.0.5 // indirect - github.com/ryanuber/go-glob v1.0.0 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect diff --git a/plugin/service/host/README.md b/plugin/service/host/README.md new file mode 100644 index 0000000..0bd7791 --- /dev/null +++ b/plugin/service/host/README.md @@ -0,0 +1,106 @@ +## Getting Started + +Refer to [Attributes and Secrets](#attributes-and-secrets) for more detail on +configuration options for host catalogs and sets. + +To create a host catalog (using default scope created by `boundary dev`): + +```sh +boundary host-catalogs create plugin \ + -scope-id p_1234567890 \ + -name "Example Plugin-Based Host Catalog" \ + -description "Description for plugin-based host catalog" \ + -plugin-name gcp \ + -attr client_email=CLIENT_EMAIL \ + -attr project_id=PROJECT_ID \ + -attr zone=ZONE \ + -secret private_key_id='PRIVATE_KEY_ID' \ + -secret private_key='PRIVATE_KEY' +``` + +To create a host set, filtering the host set based on status value +of `RUNNING` and a label. + +```sh +boundary host-sets create plugin \ + -host-catalog-id HOST_CATALOG_ID \ + -name "Example Plugin-Based Host Set" \ + -description "Description for plugin-based host set" \ + -attr filters=status=RUNNING + -attr filters=labels.env:prod +``` + +## Required IAM Permissions + +The following IAM permissions, at the very least, are required to be attached to +a configured service account for this provider: + +### List Instances + +To list instances, the credential requires these permissions: +``` +[ + "compute.instances.list" +] +``` + +### Rotate Service Account Keys + +When GCP service account keys are provided with credential rotation enabled, GCP +service account keys will be rotated when requests are made to the plugin. These +permissions are required to rotate the service account key: + +``` +[ + "iam.serviceAccountKeys.create", + "iam.serviceAccountKeys.delete", +] +``` + +### Service Account Impersonation + +For the base service account to authenticate by service account impersonation. +The base service account needs to have these roles: + +``` +[ + "iam.serviceAccountTokenCreator" +] +``` + +## Attributes and Secrets + +### Host Catalog + +The following `attributes` are valid on a GCP host catalog resource: + + +- `zone` (string, required): The zone to configure the host catalog for. All host sets + in this catalog will be configured for this zone. +- `project_id` (string, required): The project ID associated with the service account. All host sets + in this catalog will be configured for this project. +- `disable_credential_rotation` (bool): If `true`, credential rotation will not + be performed. See the [Credentials](../../../README.md#credentials) readme for more information. +- `client_email` (string): The email address associated with the service account. The email address + used to uniquely identify the service account. It is required for authentication and authorization. +- `target_service_account_id` (string): The unique identifier for the service account that will be + impersonate. This is only required when authenticating with service account impersonation. + +The following `secrets` are required on an GCP host catalog resource: + +- `private_key_id` (string): The private key ID for the service account to use with this + host catalog. +- `private_key` (string): The private key for the service account to use + with this host catalog. + +See the [Credentials](../../../README.md#credentials) readme for more information. + +### Host Set + +The following attributes are valid on a GCP host Set resource: + +- `filters` (array of strings): Host Set filters are used to narrow down the list of hosts + returned by the plugin. The filter string is expected to be in the format "key operator value". + The operator is expected to be one of =, !=, >, <, <=, >=, :, eq, ne. + as per GCP API documentation: + [instances.list reference](https://cloud.google.com/compute/docs/reference/rest/v1/instances/list#filter). \ No newline at end of file diff --git a/plugin/service/host/attributes.go b/plugin/service/host/attributes.go index e3160a7..d14c9e2 100644 --- a/plugin/service/host/attributes.go +++ b/plugin/service/host/attributes.go @@ -70,12 +70,6 @@ func getSetAttributes(in *structpb.Struct) (*SetAttributes, error) { badFields := make(map[string]string) unknownFields := values.StructFields(in) - if _, filter := unknownFields[ConstListInstancesFilter]; filter { - if _, ig := unknownFields[ConstInstanceGroup]; ig { - return nil, errors.InvalidArgumentError("Error in the attributes provided, cannot define both filter and instance group", nil) - } - } - delete(unknownFields, ConstListInstancesFilter) for a := range unknownFields { diff --git a/plugin/service/host/attributes_test.go b/plugin/service/host/attributes_test.go index fec58c3..83f4f37 100644 --- a/plugin/service/host/attributes_test.go +++ b/plugin/service/host/attributes_test.go @@ -103,14 +103,6 @@ func TestGetSetAttributes(t *testing.T) { }, expectedErrContains: "attributes.bar: unrecognized field, attributes.foo: unrecognized field", }, - { - name: "invalid, cannot have both instance filter and group defined", - in: map[string]any{ - ConstListInstancesFilter: "test", - ConstInstanceGroup: "test", - }, - expectedErrContains: "Error in the attributes provided, cannot define both filter and instance group", - }, } for _, tc := range cases { diff --git a/plugin/service/host/const.go b/plugin/service/host/const.go index 96270e1..79253fb 100644 --- a/plugin/service/host/const.go +++ b/plugin/service/host/const.go @@ -6,12 +6,8 @@ package host const ( // ConstListInstancesFilter refers to a Google Cloud SDK filter to search for instances ConstListInstancesFilter = "filters" - - // ConstInstanceGroup refers to the name of a Google Cloud instance group - ConstInstanceGroup = "instance_group" ) var allowedSetFields = map[string]struct{}{ ConstListInstancesFilter: {}, - ConstInstanceGroup: {}, } diff --git a/plugin/service/host/plugin.go b/plugin/service/host/plugin.go index 48dc475..8d49055 100644 --- a/plugin/service/host/plugin.go +++ b/plugin/service/host/plugin.go @@ -452,7 +452,7 @@ func (p *HostPlugin) OnDeleteSet(ctx context.Context, req *pb.OnDeleteSetRequest return &pb.OnDeleteSetResponse{}, nil } -// ListHosts returns the list of instances based on filter or instance group name. +// ListHosts returns the list of instances based on instance filter. func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*pb.ListHostsResponse, error) { catalog := req.GetCatalog() if catalog == nil { @@ -588,11 +588,8 @@ func validateSet(s *hostsets.HostSet) error { badFields := make(map[string]string) _, filterSet := attrMap[ConstListInstancesFilter] - _, instanceGroupSet := attrMap[ConstInstanceGroup] - if instanceGroupSet && filterSet { - badFields["attributes"] = "must set instance group or filter, cannot set both" - } else if filterSet && len(attrs.Filters) == 0 { + if filterSet && len(attrs.Filters) == 0 { badFields[fmt.Sprintf("attributes.%s", ConstListInstancesFilter)] = "must not be empty" }