Skip to content

About

Deliberately misconfigured two-tier app on GCP: a Go to-do app on GKE backed by MongoDB on a VM, with Terraform and GitHub Actions. Built for cloud security review.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

31 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

gcp-exercise

A two-tier web application deployed on Google Cloud, built as a cloud security technical exercise. The environment is deliberately misconfigured in several places so the weaknesses can be found, demonstrated and discussed. Do not use it as a template for anything real.

  • App: Tasky, a Go to-do app with JWT login, containerised and running on GKE
  • Database: MongoDB 4.4 on a Compute Engine VM, with a nightly backup to Cloud Storage
  • Infrastructure: Terraform, with state in a GCS bucket
  • Delivery: GitHub Actions pipelines for infrastructure and for the app

Architecture

                       Internet
                          │
            ┌─────────────┴───────────────┐
            │ HTTP :80                    │ SSH :22 (0.0.0.0/0)
            ▼                             ▼
  ┌───────────────────┐         ┌────────────────────┐
  │ GKE LoadBalancer  │         │ mongodb-vm         │
  │ tasky-service     │         │ Ubuntu 20.04       │
  └─────────┬─────────┘         │ MongoDB 4.4        │──► gs://<backup bucket>
            ▼                   │ public IP          │    (daily mongodump, 01:00)
  ┌───────────────────┐ :27017  │ public subnet      │
  │ tasky pod         │────────►│ 10.10.10.0/24      │
  │ private subnet    │         └────────────────────┘
  │ 10.20.0.0/24      │
  └───────────────────┘
     egress via Cloud NAT

All resources live in one VPC in europe-west1.

Component Where it's defined Notes
VPC, public + private subnets, Cloud Router/NAT, firewall rules terraform/network.tf
GKE cluster + 1-node pool (e2-medium) terraform/gke.tf Private nodes, public control-plane endpoint
MongoDB VM mongodb-vm + its service account terraform/compute.tf Bootstrapped by terraform/startup-scripts/mongodb.sh
Artifact Registry repo tasky-repo terraform/artifact_registry.tf Docker images for Tasky
Cluster-admin grant for the deployer SA terraform/iam.tf
Tasky Deployment, LoadBalancer Service, Secret, ServiceAccount, ClusterRoleBinding k8s/tasky/
Tasky source + Dockerfile tasky/

Intentional weaknesses

These are on purpose; several are flagged in comments in the code.

Area Weakness Location
Network SSH open to 0.0.0.0/0 on the DB VM network.tf
Compute DB VM has a public IP compute.tf
Compute Outdated OS (Ubuntu 20.04) and database (MongoDB 4.4) compute.tf, mongodb.sh
IAM DB VM service account holds roles/editor on the project, with cloud-platform scope compute.tf
Database MongoDB bound to 0.0.0.0; hard-coded admin and app credentials mongodb.sh
Kubernetes Tasky's service account is bound to cluster-admin k8s/tasky/cluster-role-binding.yaml
Kubernetes Secret committed to the repo in plain text (DB URI, JWT key) k8s/tasky/secret.yaml
Container Old base images (golang:1.19, alpine:3.17.0) tasky/Dockerfile
CI/CD Long-lived service-account JSON key used for GCP auth .github/workflows/*.yml

Prerequisites

  • A GCP project with billing enabled and the Compute, Kubernetes Engine and Artifact Registry APIs turned on
  • A service account named terraform-deployer with rights to create the resources above, and a JSON key for it
  • A GCS bucket for Terraform state, named in the backend block of terraform/main.tf
  • A GCS bucket for MongoDB backups. It isn't created by Terraform: create it yourself, give the MongoDB VM's service account write access, and set BUCKET_NAME in terraform/startup-scripts/mongodb.sh
  • Terraform ≥ 1.5 (CI uses 1.6.6), gcloud, kubectl and Docker for local work

Deploying

1. Infrastructure

cd terraform
cp /path/to/key.json terraform-deployer-key.json   # git-ignored; picked up by the provider if present
terraform init
terraform apply -var="project_id=<your-project-id>"

Variables (all in variables.tf) default to europe-west1 / europe-west1-b and the CIDRs shown above; only project_id is required.

The VM's startup script installs MongoDB, creates an admin user and the tasky_user app user on the go-mongodb database, enables auth, and installs a cron job that runs /usr/local/bin/mongodb-backup.sh at 01:00 daily.

2. Application

  1. Point MONGODB_URI in k8s/tasky/secret.yaml at the MongoDB VM's internal IP (currently 10.10.10.21).
  2. Update the image in k8s/tasky/deployment.yaml to your project's registry path.
  3. Build, push and apply:
REGION=europe-west1
IMAGE=$REGION-docker.pkg.dev/<project-id>/tasky-repo/<image-name>:latest

gcloud auth configure-docker $REGION-docker.pkg.dev
docker build -t $IMAGE ./tasky && docker push $IMAGE

gcloud container clusters get-credentials <cluster-name> --zone europe-west1-b
kubectl apply -f k8s/tasky/
kubectl get svc tasky-service    # EXTERNAL-IP is the app URL, port 80

CI/CD

Both workflows run on push and authenticate with the same service-account key.

terraform.yml — triggered by changes under terraform/. Runs init and plan on every branch; runs apply -auto-approve only on main.

tasky.yml — triggered by changes under tasky/ or k8s/tasky/. Builds the image tagged with the commit SHA, pushes it to Artifact Registry, applies the manifests, restarts the deployment, sets the new image and waits for rollout.

Repository secrets used:

Secret Used by Purpose
GCP_SA_KEY both Service-account JSON key
GCP_PROJECT both Project ID (TF_VAR_project_id in Terraform)
GCP_REGION tasky Artifact Registry region, e.g. europe-west1
GCP_ARTIFACT_REPO tasky Registry repo name, e.g. tasky-repo
GKE_CLUSTER tasky GKE cluster name
GKE_ZONE tasky europe-west1-b

Running Tasky locally

cd tasky
cat > .env <<EOF
MONGODB_URI=mongodb://localhost:27017
SECRET_KEY=change-me
EOF
go mod tidy
go run main.go      # http://localhost:8080

See tasky/README.md for more on the app itself.

Verifying the deployment

# Pod's service account has cluster-admin
kubectl auth can-i '*' '*' --as=system:serviceaccount:default:tasky-service-account

# Backups are landing in the bucket
gsutil ls gs://<backup-bucket>/

Tearing down

kubectl delete -f k8s/tasky/     # removes the LoadBalancer first
cd terraform && terraform destroy -var="project_id=<your-project-id>"

The state bucket and backup bucket are outside Terraform and need deleting by hand.

About

Deliberately misconfigured two-tier app on GCP: a Go to-do app on GKE backed by MongoDB on a VM, with Terraform and GitHub Actions. Built for cloud security review.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages