Last active
May 2, 2025 17:53
-
-
Save mikesparr/090a1f94bf24286a953d89a37874110f to your computer and use it in GitHub Desktop.
Google Cloud Organization Initial Setup
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
BILLING="YOUR-BILLING-ACCT" | |
ORGANIZATION="<ORG-ID-NUMBER>" | |
FOLDER="<FOLDER-ID-NUMBER>" | |
CUSTOMER="<CUSTOMER-ID>" | |
# user groups | |
export ORG_ADMIN_GROUP="[email protected]" | |
export BILLING_ADMIN_GROUP="[email protected]" | |
export SECURITY_ADMIN_GROUP="[email protected]" | |
export NETWORK_ADMIN_GROUP="[email protected]" | |
export DEVELOPER_GROUP="[email protected]" | |
export DEVOPS_GROUP="[email protected]" | |
# shared projects | |
BILLING_PROJECT_ID="billing" | |
SECURITY_PROJECT_ID="security" | |
MONITORING_PROJECT_ID="monitoring" | |
DEVOPS_PROJECT_ID="devops" | |
# service projects | |
SANDBOX_PROJECT_ID="myco-sandbox" | |
DATA_SCIENCE_PROJECT_ID="myco-data-science" | |
BACKEND_STAGE_PROJECT_ID="myco-backend-stage" | |
BACKEND_PROD_PROJECT_ID="myco-backend-prod" | |
FRONTEND_STAGE_PROJECT_ID="myco-frontend-stage" | |
FRONTEND_PROD_PROJECT_ID="myco-frontend-prod" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# --- Configuration --- | |
source .env | |
# --- Helper Function (optional, for cleaner output) --- | |
apply_policy() { | |
local constraint_name="$1" | |
local policy_file="$2" | |
local description="$3" | |
echo "-----------------------------------------------------" | |
echo "Applying Policy: $description ($constraint_name)" | |
echo "Policy File Content:" | |
cat "$policy_file" | |
echo "" | |
gcloud resource-manager org-policies set-policy "$policy_file" --organization="$ORGANIZATION" | |
if [[ $? -eq 0 ]]; then | |
echo "SUCCESS: Applied policy $constraint_name." | |
else | |
echo "ERROR: Failed to apply policy $constraint_name. Check permissions and configuration." | |
# Consider adding 'exit 1' here if you want the script to stop on failure | |
fi | |
rm "$policy_file" # Clean up temporary file | |
echo "-----------------------------------------------------" | |
echo "" | |
} | |
# --- Check if ORGANIZATION and CUSTOMER are set --- | |
if [[ "$ORGANIZATION" == "<ORG-ID-NUMBER>" || "$CUSTOMER" == "<CUSTOMER-ID>" ]]; then | |
echo "ERROR: Please replace the placeholder values for ORGANIZATION and CUSTOMER in .env file." | |
exit 1 | |
fi | |
# --- 1. Restrict VM External IP Access --- | |
POLICY_FILE_VM_IP="policy_vm_external_ip.yaml" | |
cat << EOF > "$POLICY_FILE_VM_IP" | |
constraint: constraints/compute.vmExternalIpAccess | |
listPolicy: | |
allValues: DENY | |
EOF | |
apply_policy "constraints/compute.vmExternalIpAccess" "$POLICY_FILE_VM_IP" "Restrict VM External IP Access" | |
# --- 2. Enforce Public Access Prevention for Cloud Storage --- | |
POLICY_FILE_GCS_PUBLIC="policy_gcs_public.yaml" | |
cat << EOF > "$POLICY_FILE_GCS_PUBLIC" | |
constraint: constraints/storage.publicAccessPrevention | |
booleanPolicy: | |
enforced: true | |
EOF | |
apply_policy "constraints/storage.publicAccessPrevention" "$POLICY_FILE_GCS_PUBLIC" "Enforce Storage Public Access Prevention" | |
# --- 3. Enforce Uniform Bucket-level Access for Cloud Storage --- | |
POLICY_FILE_GCS_PUBLIC="policy_gcs_uniform.yaml" | |
cat << EOF > "$POLICY_FILE_GCS_PUBLIC" | |
constraint: constraints/storage.uniformBucketLevelAccess | |
booleanPolicy: | |
enforced: true | |
EOF | |
apply_policy "constraints/storage.uniformBucketLevelAccess" "$POLICY_FILE_GCS_PUBLIC" "Require Uniform Bucket-level Access" | |
# --- 4. Disable Service Account Key Creation --- | |
POLICY_FILE_SA_KEYS="policy_sa_keys.yaml" | |
cat << EOF > "$POLICY_FILE_SA_KEYS" | |
constraint: constraints/iam.disableServiceAccountKeyCreation | |
booleanPolicy: | |
enforced: true | |
EOF | |
apply_policy "constraints/iam.disableServiceAccountKeyCreation" "$POLICY_FILE_SA_KEYS" "Disable Service Account Key Creation" | |
# POLICY_FILE_SA_KEYS_MNG="policy_sa_keys_managed.yaml" | |
# cat << EOF > "$POLICY_FILE_SA_KEYS_MNG" | |
# constraint: constraints/iam.managed.disableServiceAccountKeyCreation | |
# booleanPolicy: | |
# enforced: true | |
# EOF | |
# apply_policy "constraints/iam.managed.disableServiceAccountKeyCreation" "$POLICY_FILE_SA_KEYS_MNG" "Disable Service Account Key Creation (Managed)" | |
# --- 5. Disable Service Account Key Upload --- | |
POLICY_FILE_SA_KEYS_UP="policy_sa_keys_up.yaml" | |
cat << EOF > "$POLICY_FILE_SA_KEYS_UP" | |
constraint: constraints/iam.disableServiceAccountKeyUpload | |
booleanPolicy: | |
enforced: true | |
EOF | |
apply_policy "constraints/iam.disableServiceAccountKeyUpload" "$POLICY_FILE_SA_KEYS_UP" "Disable Service Account Key Upload" | |
# POLICY_FILE_SA_KEYS_UP_MNG="policy_sa_keys_up_managed.yaml" | |
# cat << EOF > "$POLICY_FILE_SA_KEYS_UP_MNG" | |
# constraint: constraints/iam.managed.disableServiceAccountKeyUpload | |
# booleanPolicy: | |
# enforced: true | |
# EOF | |
# apply_policy "constraints/iam.managed.disableServiceAccountKeyUpload" "$POLICY_FILE_SA_KEYS_UP_MNG" "Disable Service Account Key Upload (Managed)" | |
# --- 6. Restrict Allowed Domains for IAM Policies --- | |
POLICY_FILE_IAM_DOMAINS="policy_iam_domains.yaml" | |
cat << EOF > "$POLICY_FILE_IAM_DOMAINS" | |
constraint: constraints/iam.allowedPolicyMemberDomains | |
listPolicy: | |
allowedValues: | |
- $CUSTOMER | |
EOF | |
apply_policy "constraints/iam.allowedPolicyMemberDomains" "$POLICY_FILE_IAM_DOMAINS" "Restrict Allowed IAM Member Domains" | |
# --- 7. Disable Serial Port Access --- | |
POLICY_FILE_SERIAL="policy_serial_port.yaml" | |
cat << EOF > "$POLICY_FILE_SERIAL" | |
constraint: constraints/compute.disableSerialPortAccess | |
booleanPolicy: | |
enforced: true | |
EOF | |
apply_policy "constraints/compute.disableSerialPortAccess" "$POLICY_FILE_SERIAL" "Disable Serial Port Access" | |
# --- 8. Skip Default Network Creation --- | |
POLICY_FILE_DEFAULT_NET="policy_default_network.yaml" | |
cat << EOF > "$POLICY_FILE_DEFAULT_NET" | |
constraint: constraints/compute.skipDefaultNetworkCreation | |
booleanPolicy: | |
enforced: true | |
EOF | |
apply_policy "constraints/compute.skipDefaultNetworkCreation" "$POLICY_FILE_DEFAULT_NET" "Skip Default Network Creation" | |
echo "All foundational organization policies applied." | |
echo "Verification: Use 'gcloud resource-manager org-policies describe CONSTRAINT_NAME --organization=$ORGANIZATION' for each constraint." | |
echo "Example: gcloud resource-manager org-policies describe constraints/compute.vmExternalIpAccess --organization=$ORGANIZATION" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
##################################################################### | |
# REFERENCES | |
# - https://cloud.google.com/resource-manager/docs/organization-policy/org-policy-constraints | |
# - https://cloud.google.com/logging/docs/central-log-storage | |
# - https://cloud.google.com/logging/docs/export/configure_export_v2 | |
# - https://cloud.google.com/billing/docs/how-to/export-data-bigquery-setup | |
# - https://cloud.google.com/artifact-registry/docs/docker/store-docker-container-images | |
# - https://cloud.google.com/monitoring/settings/manage-api | |
# - https://cloud.google.com/sdk/gcloud/reference/apphub | |
##################################################################### | |
export PROJECT_ID=$(gcloud config get-value project) | |
export PROJECT_USER=$(gcloud config get-value core/account) # set current user | |
export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format="value(projectNumber)") | |
export IDNS=${PROJECT_ID}.svc.id.goog # workflow identity domain | |
export GCP_REGION="us-central1" # CHANGEME (OPT) | |
export GCP_ZONE="us-central1-a" # CHANGEME (OPT) | |
export NETWORK_NAME="default" | |
# configure gcloud sdk | |
gcloud config set compute/region $GCP_REGION | |
gcloud config set compute/zone $GCP_ZONE | |
# projects | |
export BILLING_PROJECT_ID="billing" # CHANGEME (IDENTIFY YOUR ACTUAL PROJECT ID) | |
export SECURITY_PROJECT_ID="security" # CHANGEME (IDENTIFY YOUR ACTUAL PROJECT ID) | |
export MONITORING_PROJECT_ID="monitoring" # CHANGEME (IDENTIFY YOUR ACTUAL PROJECT ID) | |
export DEVOPS_PROJECT_ID="devops" # CHANGEME (IDENTIFY YOUR ACTUAL PROJECT ID) | |
# env (billing, organization, folder, customer, project IDs) | |
source .env | |
######################################################## | |
# ORGANIZATION | |
######################################################## | |
export DOMAIN="example.com" | |
# apply org policies | |
echo "Applying foundational org policies..." | |
./policies.sh | |
echo "Applying foundational org policies... DONE!" | |
# remove default global roles (project and billing account) | |
gcloud organizations remove-iam-policy-binding $ORGANIZATION \ | |
--member="domain:$DOMAIN" \ | |
--role="roles/resourcemanager.projectCreator" \ | |
--condition=None | |
gcloud organizations remove-iam-policy-binding $ORGANIZATION \ | |
--member="domain:$DOMAIN" \ | |
--role="roles/billing.user" \ | |
--condition=None | |
######################################################## | |
# SECURITY | |
######################################################## | |
export SECURITY_BUCKET_NAME="myco-audit-logs" | |
export SECURITY_BUCKET_LOCATION="US" | |
export LOG_SINK_NAME="audit-logs-sink" | |
export LOG_SINK_BUCKET_PATH="storage.googleapis.com/$SECURITY_BUCKET_NAME" | |
# enable services apis | |
gcloud services enable compute.googleapis.com \ | |
storage.googleapis.com \ | |
--project $SECURITY_PROJECT_ID | |
# create bucket | |
gcloud storage buckets create gs://$SECURITY_BUCKET_NAME \ | |
--public-access-prevention \ | |
--uniform-bucket-level-access \ | |
--location $SECURITY_BUCKET_LOCATION \ | |
--project $SECURITY_PROJECT_ID | |
# update bucket to clear soft delete | |
gcloud storage buckets update gs://$SECURITY_BUCKET_NAME \ | |
--clear-soft-delete \ | |
--project $SECURITY_PROJECT_ID | |
# create aggregated audit log sink | |
gcloud logging sinks create $LOG_SINK_NAME $LOG_SINK_BUCKET_PATH \ | |
--log-filter='logName:cloudaudit.googleapis.com' \ | |
--description="Audit logs from my organization" \ | |
--organization=$ORGANIZATION \ | |
--include-children | |
# fetch sink writer identity | |
LOG_WRITER_IDENTITY=$(gcloud logging sinks describe $LOG_SINK_NAME --organization $ORGANIZATION --format="value(writerIdentity)") | |
echo $LOG_WRITER_IDENTITY | |
# authorize writer identity on destination bucket | |
gcloud storage buckets add-iam-policy-binding gs://$SECURITY_BUCKET_NAME \ | |
--member=$LOG_WRITER_IDENTITY \ | |
--role="roles/storage.objectCreator" | |
######################################################## | |
# BILLING | |
######################################################## | |
export BILLING_DATASET_NAME="billing_export" | |
# enable services apis | |
gcloud services enable compute.googleapis.com \ | |
bigquery.googleapis.com \ | |
bigquerydatatransfer.googleapis.com \ | |
--project $BILLING_PROJECT_ID | |
# create dataset | |
bq --location=$GCP_REGION mk -d \ | |
--description "Billing data export." \ | |
--project_id $BILLING_PROJECT_ID \ | |
$BILLING_DATASET_NAME | |
# Billing export (enabled in console and selected configured bucket) | |
######################################################## | |
# MONITORING (TBD) | |
######################################################## | |
# add projects to monitoring metrics scope | |
gcloud beta monitoring metrics-scopes create "projects/$DEVOPS_PROJECT_ID" \ | |
--project $MONITORING_PROJECT_ID | |
# repeat for engineering projects (less the AppHub mgmt project) | |
gcloud beta monitoring metrics-scopes create "projects/$SANDBOX_PROJECT_ID" \ | |
--project $MONITORING_PROJECT_ID | |
gcloud beta monitoring metrics-scopes create "projects/$DATA_SCIENCE_PROJECT_ID" \ | |
--project $MONITORING_PROJECT_ID | |
gcloud beta monitoring metrics-scopes create "projects/$BACKEND_STAGE_PROJECT_ID" \ | |
--project $MONITORING_PROJECT_ID | |
gcloud beta monitoring metrics-scopes create "projects/$BACKEND_PROD_PROJECT_ID" \ | |
--project $MONITORING_PROJECT_ID | |
gcloud beta monitoring metrics-scopes create "projects/$FRONTEND_STAGE_PROJECT_ID" \ | |
--project $MONITORING_PROJECT_ID | |
gcloud beta monitoring metrics-scopes create "projects/$FRONTEND_PROD_PROJECT_ID" \ | |
--project $MONITORING_PROJECT_ID | |
######################################################## | |
# DEVOPS | |
######################################################## | |
export TF_STATE_BUCKET_NAME="myco-tf-state" | |
export TF_STATE_BUCKET_LOCATION="US" | |
export TF_SERVICE_ACCOUNT_NAME="tf-admin" | |
export REPO_NAME="myco-docker-repo" | |
export DEVOPS_PROJECT_NUMBER=$(gcloud projects describe $DEVOPS_PROJECT_ID --format="value(projectNumber)") | |
export CLOUD_BUILD_SERVICE_AGENT="service-$DEVOPS_PROJECT_NUMBER@gcp-sa-cloudbuild.iam.gserviceaccount.com" | |
export CONNECTION_NAME="myco-source-repo" | |
# enable services apis | |
gcloud services enable compute.googleapis.com \ | |
storage.googleapis.com \ | |
artifactregistry.googleapis.com \ | |
cloudbuild.googleapis.com \ | |
secretmanager.googleapis.com \ | |
cloudresourcemanager.googleapis.com \ | |
--project $DEVOPS_PROJECT_ID | |
# create bucket | |
gcloud storage buckets create gs://$TF_STATE_BUCKET_NAME \ | |
--public-access-prevention \ | |
--uniform-bucket-level-access \ | |
--location $TF_STATE_BUCKET_LOCATION \ | |
--project $DEVOPS_PROJECT_ID | |
# update bucket (removed soft delete) | |
gcloud storage buckets update gs://$TF_STATE_BUCKET_NAME \ | |
--clear-soft-delete \ | |
--project $DEVOPS_PROJECT_ID | |
# create TF service account | |
gcloud iam service-accounts create $TF_SERVICE_ACCOUNT_NAME \ | |
--description="Terraform Admin Service Account" \ | |
--display-name="$TF_SERVICE_ACCOUNT_NAME" \ | |
--project $DEVOPS_PROJECT_ID | |
# grant roles TF should be able to perform with (permissive) | |
gcloud projects add-iam-policy-binding $DEVOPS_PROJECT_ID \ | |
--member="serviceAccount:$TF_SERVICE_ACCOUNT_NAME@$DEVOPS_PROJECT_ID.iam.gserviceaccount.com" \ | |
--role="roles/storage.objectCreator" \ | |
--role="roles/iam.serviceAccountTokenCreator" \ | |
--role="roles/cloudbuild.builds.editor" \ | |
--role="roles/cloudbuild.connectionAdmin" \ | |
--role="roles/artifactregistry.admin" | |
gcloud resource-manager folders add-iam-policy-binding $ENGINEERING_FOLDER_ID \ | |
--member="serviceAccount:$TF_SERVICE_ACCOUNT_NAME@$DEVOPS_PROJECT_ID.iam.gserviceaccount.com" \ | |
--role="roles/apigateway.admin" \ | |
--role="roles/iam.serviceAccountTokenCreator" \ | |
--role="roles/iam.serviceAccountAdmin" \ | |
--role="roles/iam.securityAdmin" \ | |
--role="roles/servicenetworking.networksAdmin" \ | |
--role="roles/cloudfunctions.admin" \ | |
--role="roles/run.builder" \ | |
--role="roles/compute.instanceAdmin" \ | |
--role="roles/compute.loadBalancerAdmin" \ | |
--role="roles/compute.publicIpAdmin" \ | |
--role="roles/compute.storageAdmin" \ | |
--role="roles/compute.networkAdmin" \ | |
--role="roles/compute.securityAdmin" \ | |
--role="roles/monitoring.admin" \ | |
--role="roles/storage.admin" \ | |
--role="roles/pubsub.admin" \ | |
--role="roles/pubsublite.admin" \ | |
--role="roles/logging.admin" \ | |
--role="roles/secretmanager.admin" \ | |
--role="roles/dns.admin" \ | |
--role="roles/cloudkms.admin" \ | |
--role="roles/cloudtrace.admin" \ | |
--role="roles/cloudsql.admin" | |
# grant devops group ability to impersonate TF SA | |
gcloud projects add-iam-policy-binding $DEVOPS_PROJECT_ID \ | |
--member="group:$DEVOPS_GROUP" \ | |
--role="roles/iam.serviceAccountTokenCreator" \ | |
--condition=None | |
# grant cloud build service agent secret access | |
gcloud projects add-iam-policy-binding $DEVOPS_PROJECT_ID \ | |
--member="serviceAccount:$CLOUD_BUILD_SERVICE_AGENT" \ | |
--role="roles/secretmanager.admin" \ | |
--condition=None | |
# create github connection | |
gcloud builds connections create github $CONNECTION_NAME \ | |
--region=$GCP_REGION \ | |
--project $DEVOPS_PROJECT_ID | |
# link manually then confirm | |
gcloud builds connections describe $CONNECTION_NAME \ | |
--region $GCP_REGION \ | |
--project $DEVOPS_PROJECT_ID | |
# artifact registry | |
gcloud artifacts repositories create $REPO_NAME \ | |
--repository-format=docker \ | |
--location=$GCP_REGION \ | |
--description="Docker repository" \ | |
--project $DEVOPS_PROJECT_ID | |
# authorize local docker to push | |
gcloud auth configure-docker $GCP_REGION-docker.pkg.dev |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Organization Setup
This Gist is simply what I personally used to bootstrap the Google Cloud organization of my latest startup, PetPublish, prior to a friend stepping in to help IaC-ify our infra and help build our CI/CD pipelines, monitoring, tracing, etc.
At my prior company I authored many comprehensive and opinionated Google Cloud best practice guides / checklists, and other resources. You're welcome to check some of them out, which will hint towards what this Gist scaffolds.
What does this solve?
It makes your cloud posture more secure right from the start. Google Cloud obfuscates a lot of the cloud hosting complexity from users with some intelligent default settings so they can get started right away. Unfortunately, however, as you scale or need to enhance your security posture for compliance (or remediation), there are some best practices that should be followed. This applies those practices to the organization from the start, reducing the rework and pain later.
Key enhancements
Example org structure
This is how I organized my latest startup leveraging folders (like AWS OU) and projects (like AWS account). The shared-services folder includes standard projects I always include for accommodating the above. You can add them at any time but it's best to start and there's no cost to have projects, only the eventual costs for running resources, which are minimal (cheap storage mostly).
Centralized monitoring
I'm still researching the new Preview feature called App Hub and App-enabled Folders but we did allow app-enabled folders for the
engineering
folder in the diagram above. Normally I centralize monitoring so future production support staff could gain access to troubleshoot without necessarily being granted access to production (another compliance thing).Billing export to BigQuery
There is the possibly for more verbose pricing export as well, but I just chose the first 2 as illustrated to ship to our
billing
project and BigQuery datasetbilling_export
.