GCP PubSub¶
This guide explains how to set up GCP PubSub listener in order to allow Just In Time Rotation for GCP Secrets Manager.
How it Works¶
reloader will listen for audit log events from a given GCP PubSub, and, based on pattern matching,
will trigger an ExternalSecret reconciliation if that object queries the GCP Secret Manager key that had its version updated.
Setting Up GCP¶
In order to set up GCP, four steps are needed:
- Create a Topic and a Subscription on GCP Pub/Sub
- Create a Log Router to send information from Audit Logs to PubSub
- Create a Service Account & Permissions for
reloader - Install
reloaderin your cluster
Tip
The following terraform code contains a recipe for everything needed within GCP using service account keys. We recommend using it with a local kind cluster for a quick onboarding experience - use workload identity in production environments.
The only action remaining is to install reloader within your cluster.
Enable Service APIs
PROJECT_ID=your-project-id
gcloud services enable pubsub.googleapis.com --project $PROJECT_ID
gcloud services enable secretmanager.googleapis.com --project $PROJECT_ID
gcloud services enable logging.googleapis.com --project $PROJECT_ID
resource "google_project_service" "pubsub" {
project = var.project_id
service = "pubsub.googleapis.com"
}
resource "google_project_service" "secretmanager" {
project = var.project_id
service = "secretmanager.googleapis.com"
}
resource "google_project_service" "logging" {
project = var.project_id
service = "logging.googleapis.com"
}
Create a Topic and a Subscription on GCP PubSub
PROJECT_ID=your-project-id
TOPIC_ID=your-topic-id
SUBSCRIPTION_ID=your-subscription-id
gcloud pubsub topics create $TOPIC_ID --project $PROJECT_ID
gcloud pubsub subscriptions create $SUBSCRIPTION_ID \
--topic $TOPIC_ID \
--project $PROJECT_ID
variable "project_id" {
type = string
}
variable "topic_name" {
type = string
default = "your-topic-id"
}
variable "subscription_name" {
type = string
default = "your-subscription-id"
}
provider "google" {
project = var.project_id
region = "us-central1"
}
resource "google_pubsub_topic" "topic" {
name = var.topic_name
}
resource "google_pubsub_subscription" "subscription" {
name = var.subscription_name
topic = google_pubsub_topic.topic.name
}
Create a Log Router for the PubSub Topic
PROJECT_ID=your-project-id
TOPIC_ID=your-topic-id
SUBSCRIPTION_ID=your-subscription-id
SINK_NAME=secret-manager-addsecretversion-sink
gcloud logging sinks create $SINK_NAME \
pubsub.googleapis.com/projects/$PROJECT_ID/topics/$TOPIC_ID \
--log-filter 'protoPayload.methodName=~"google.cloud.secretmanager.v1.SecretManagerService.AddSecretVersion"' \
--project $PROJECT_ID
SINK_SERVICE_ACCOUNT=$(gcloud logging sinks describe $SINK_NAME \
--format 'value(writerIdentity)' \
--project $PROJECT_ID)
gcloud pubsub topics add-iam-policy-binding $TOPIC_ID \
--member $SINK_SERVICE_ACCOUNT \
--role roles/pubsub.publisher \
--project $PROJECT_ID
variable "sink_name" {
type = string
default = "secret-manager-addsecretversion-sink"
}
resource "google_logging_project_sink" "log_sink" {
name = var.sink_name
destination = "pubsub.googleapis.com/projects/${var.project_id}/topics/${google_pubsub_topic.topic.name}"
filter = <<EOF
protoPayload.methodName=~"google.cloud.secretmanager.v1.SecretManagerService.AddSecretVersion"
EOF
}
resource "google_pubsub_topic_iam_member" "sink_publisher" {
topic = google_pubsub_topic.topic.name
role = "roles/pubsub.publisher"
member = google_logging_project_sink.log_sink.writer_identity
}
Create Service Account & Permissions (Service Account Keys)
SA_NAME=reloader-sa
PROJECT_ID=your-project-id
gcloud iam service-accounts create $SA_NAME \
--display-name "Service Account for Reloader" \
--project $PROJECT_ID
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member "serviceAccount:$SA_NAME@$PROJECT_ID.iam.gserviceaccount.com" \
--role roles/pubsub.subscriber
gcloud iam service-accounts keys create key.json \
--iam-account "$SA_NAME@$PROJECT_ID.iam.gserviceaccount.com"
echo "now, make sure you install this key in the cluster so that reloader can use it"
variable "service_account_name" {
type = string
default = "reloader-sa"
}
resource "google_service_account" "reloader_sa" {
account_id = var.service_account_name
display_name = "Service Account for Reloader"
}
resource "google_project_iam_member" "pubsub_subscriber_sa_key" {
project = var.project_id
role = "roles/pubsub.subscriber"
member = "serviceAccount:${google_service_account.reloader_sa.email}"
}
resource "google_project_iam_member" "secret_accessor_sa_key" {
project = var.project_id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_service_account.reloader_sa.email}"
}
resource "google_service_account_key" "sa_key" {
service_account_id = google_service_account.reloader_sa.name
public_key_type = "TYPE_X509_PEM_FILE"
private_key_type = "TYPE_GOOGLE_CREDENTIALS_FILE"
}
Create Service Account & Permissions (Workload Identity)
SA_NAME=reloader-sa
PROJECT_ID=your-project-id
NAMESPACE=service-account-namespace
KSA_NAME=service-account-name
gcloud iam service-accounts create $SA_NAME \
--display-name "Service Account for Reloader" \
--project $PROJECT_ID
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member "serviceAccount:$SA_NAME@$PROJECT_ID.iam.gserviceaccount.com" \
--role roles/pubsub.subscriber
gcloud iam service-accounts add-iam-policy-binding $SA_NAME@$PROJECT_ID.iam.gserviceaccount.com \
--member "serviceAccount:$PROJECT_ID.svc.id.goog[$NAMESPACE/$KSA_NAME]" \
--role roles/iam.workloadIdentityUser
kubectl create serviceaccount $KSA_NAME --namespace $NAMESPACE
kubectl annotate serviceaccount $KSA_NAME \
--namespace $NAMESPACE \
iam.gke.io/gcp-service-account=$SA_NAME@$PROJECT_ID.iam.gserviceaccount.com
variable "kubernetes_service_account_name" {
type = string
default = "reloader-ksa"
}
variable "namespace" {
type = string
default = "your-namespace"
}
variable "cluster_name" {
type = string
}
variable "cluster_location" {
type = string
}
provider "kubernetes" {
host = google_container_cluster.gke_cluster.endpoint
token = data.google_client_config.default.access_token
cluster_ca_certificate = base64decode(
google_container_cluster.gke_cluster.master_auth[0].cluster_ca_certificate,
)
}
data "google_client_config" "default" {}
resource "kubernetes_service_account" "ksa" {
metadata {
name = var.kubernetes_service_account_name
namespace = var.namespace
annotations = {
"iam.gke.io/gcp-service-account" = google_service_account.reloader_sa.email
}
}
}
resource "google_service_account" "reloader_sa" {
account_id = var.service_account_name
display_name = "Service Account for Reloader"
}
resource "google_service_account_iam_member" "workload_identity_binding" {
service_account_id = google_service_account.reloader_sa.name
role = "roles/iam.workloadIdentityUser"
member = "serviceAccount:${var.project_id}.svc.id.goog[${var.namespace}/${var.kubernetes_service_account_name}]"
}
resource "google_project_iam_member" "pubsub_subscriber_wi" {
project = var.project_id
role = "roles/pubsub.subscriber"
member = "serviceAccount:${google_service_account.reloader_sa.email}"
}
resource "google_project_iam_member" "secret_accessor_wi" {
project = var.project_id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_service_account.reloader_sa.email}"
}
After all these steps - we are ready to install & use reloader.
Example Configuration¶
Tip
Before applying this manifest, be sure to have installed reloader first.
apiVersion: reloader.external-secrets.io/v1alpha1
kind: Config
metadata:
name: gcp-sample
spec:
notificationSources:
- type: GooglePubSub
googlePubSub:
subscriptionID: value-from-subscription-id
projectID: value-from-project-id
auth:
# If using service account keys
secretRef:
secretAccessKeySecretRef:
name: secret-name
namespace: secret-namespace
key: creds_json
# If using Workload Identity
workloadIdentity:
clusterName: name-of-your-cluster-if-on-different-project
clusterLocation: us-east1
clusterProjectID: cluster-project-if-different
serviceAccountRef:
name: service-account-name
namespace: service-account-namespace
destinationsToWatch:
- type: ExternalSecret
externalSecret:
labelSelectors:
matchLabels:
app: my-app
After that, every new AddSecretVersion message will automatically update your Kubernetes Secret values!