├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.MD ├── config.tfvars ├── debug.tf ├── gitlab-runner ├── dockerhub-values.yaml ├── kaniko-values.yaml ├── linux-values.yaml └── win-values.yaml ├── gke.tf ├── iam.tf ├── img ├── scallops-infra.png └── scallops.png ├── load-balancer.tf ├── locals.tf ├── main.tf ├── network.tf ├── outputs.tf ├── provider.tf ├── scripts ├── Powershell │ └── disabledefender.ps1 └── bash │ ├── gcloud_logger.sh │ ├── gitlab_backup.sh │ ├── gitlab_helpers.sh │ └── gitlab_startup.sh ├── secrets.tf ├── storage.tf ├── vars.tf └── versions.tf /.gitignore: -------------------------------------------------------------------------------- 1 | # Local .terraform directories 2 | **/.terraform/* 3 | 4 | # .tfstate files 5 | *.tfstate 6 | *.tfstate.* 7 | 8 | # Crash log files 9 | crash.log 10 | 11 | # Ignore override files as they are usually used to override resources locally and so 12 | # are not checked in 13 | override.tf 14 | override.tf.json 15 | *_override.tf 16 | *_override.tf.json 17 | 18 | 19 | # Exclude Terraform lock file 20 | .terraform.lock.hcl 21 | 22 | #Exclude state and config files 23 | 24 | backend.tf 25 | current*.tfvars 26 | 27 | # Ignore downloaded backups 28 | gitlab_backups/** 29 | 30 | # Certificates 31 | *.crt 32 | *.key 33 | 34 | # k8s 35 | kubeconfig -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | 4 | ## [0.6] - 2024-08-19 5 | ### Added 6 | - Added LB resource and related resources 7 | - Added application external Certificate service 8 | - Modify DNS record set to LB IP instead of Gitlab instance 9 | - Assigned Gitlab to instance group 10 | - Added option to specify external integration IP addresses. 11 | - Upgraded google terraform provider 12 | 13 | 14 | ## [0.5] - 2023-07-19 15 | ### Added 16 | - Enabled runner session server on linux-runner to allow the use of Interactive Web Terminal (Job Debug button) 17 | 18 | ### Changed 19 | - Shortened runner poll intervals 20 | - Optimize K8s performance 21 | - Upgraded GKE to version 1.26.5 22 | - Containers are now stored in Artifact Registry instead of deprecating Container Registry (GCR). 23 | - Modified node pools disk type to SSD 24 | - Modified Linux node image type to COS_CONTAINERD 25 | - Enabled image streaming 26 | - Upgraded related providers and modules 27 | - Modified k8s pdb resources according to provider upgrades 28 | - Gitlab 16.1.2 upgrade 29 | - Default to Gitlab 16.1.2-ee and runner 0.54.0 30 | - Option to set target Gitlab version to upgrade the application from tf vars 31 | - Added relevant locals 32 | - Added multiple ubuntu release options for the instance 33 | - Changed runner TOML config to be set from terraform instead of Helm chart Yaml. 34 | - Modified Gitlab instance disk to 160G SSD. 35 | - Added Gitlab DEB package link metadata variable. 36 | 37 | ### Fixed 38 | - Backup creation bug. The created backup snapshot was not deleted after upload, causing overload to the local disk. 39 | - Modified IAM binding to add memeber instead of overwrite all members on user resources. 40 | - Fixed Gitlab restore bug via new exec-wrapper function. 41 | 42 | ## [0.4] - 2022-10-22 43 | ### Added 44 | - Option to specify Scallop-Recipes repo URL (For auto import) 45 | - Error handling for Gitlab installation and backup Bash scripts 46 | - Deployment TF debug option - IAP firewall rules, export configuration files 47 | - Integration with GCP logger for Gitlab installation and backup 48 | 49 | ### Changed 50 | - README + architecture image 51 | - Updated K8s RBAC permissions for runners 52 | - Removed generated backup password. Password secret is supplied by the user. 53 | - Updated tfvars template 54 | - Upgraded Gitlab, K8s, Runners versions 55 | - Adjusted instance level CI/CD variables names 56 | - Upgraded Windows node pool machine type to 4 vCPU & 16GB RAM 57 | - Now limiting concurrent jobs for 30 in Windows and 50 in Linux 58 | - K8s nodes are now preemptible 59 | 60 | ### Fixed 61 | - Fixed TF resources dependencies 62 | - Fixed Defender disarm detection issue 63 | - Fixed K8s Scale up issues 64 | 65 | ## [0.3] - 2022-01-07 66 | ### Added 67 | - Added support to migrate from an older Gitlab instance 68 | - Added Gitlab instance automatic backup capability 69 | - K8s Pod disruption budget resources to fix scale down issue 70 | - New runner that supports Dockerhub credentials to pull private images 71 | 72 | ### Changed 73 | - Modified max pods per node in K8s, meanning more CI jobs simoultaneously 74 | - Scopred bucket admin permissions to conainer registry bucket only 75 | - Reorganized resources into seperate files 76 | - Increased CPUs on linux pool 77 | - Removed Gitlab API token seeding. Gitlab modifications performed using gitlab-rails console 78 | 79 | ### Fixed 80 | - Windows Runner configuration 81 | - Minor fixes, duplicate code removal 82 | - Fixed Linux won't scale down problem 83 | 84 | 85 | ## [0.2] - 2021-12-21 86 | ### Fixed 87 | - Windows Runner configuration 88 | - Relativ paths 89 | - Runner version update 90 | 91 | 92 | ## [0.1] - 2021-08-09 93 | ### Initial 94 | - Initial working deployment 95 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 SYGNIA 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=plastic)](https://opensource.org/licenses/MIT) 2 | ![Terraform](https://img.shields.io/badge/Terraform-v1.5.0-8040c9.svg?style=plastic) 3 | ![Gitlab](https://img.shields.io/badge/Gitlab-v16.1.2-fd7e14.svg?style=plastic) 4 | ![GKE](https://img.shields.io/badge/GKE-1.26.5-1A73E8.svg?style=plastic) 5 | ![ScallOps](https://img.shields.io/badge/ScallOps-v0.5-000000.svg?style=plastic) 6 | 7 | # SCALLOPS 8 | 9 | ## Overview 10 | 11 | ScallOps is a framework that empowers Red Teams to put more focus on what they need to do, instead of how to do it. 12 | It utilizes the CI/CD concept to manage and automate the weaponization and deployment of offensive tools. 13 | 14 | Security teams and individuals can develop, collaborate and utilize the framework's "Recipes" in order to perform their Red Team tasks with greater efficiency. 15 | 16 | You can choose to deploy the framework's infrastructure using the Terraform scripts within this repository or use your own infrastructure. 17 | Refer to the [ScallOps-Recipes](https://github.com/SygniaLabs/ScallOps-Recipes) repository to learn more about the infrasctrucure requirements. 18 | 19 | 20 |

21 | ScallOps logo 22 |

23 | 24 | ## Infrastructure Features 25 | * All Gitlab's CI/CD features for designning the Recipes. 26 | * Linux & Windows based operating systems for running the Recipes weaponization jobs. 27 | * On-demand automated node scalability (Google Kubernets Engine). 28 | * Access to work with private container images from your Google Artifact Registry. 29 | * Ability to supply Docker Hub credentials and work with private container images from your Docker Hub account. 30 | * Capability to automatically build Dockerfiles and push the images to your Artifact Registry. 31 | * Automated, weekly Gitlab backup to a desginated bucket. 32 | * Attach external hostname to the framework using Google DNS managed zones. 33 | * Ability to migrate from an older Gitlab instance or upgrade a deployed instance. 34 | 35 | ## Deployment 36 | 37 | The infrastructure can be deployed to GCP using the provided Terraform scripts. 38 | It is mainly built from a Gitlab instance that provides the CI features and a Kubernetes cluster that execute CI jobs on the relevant operating systems. 39 | There is also a use of Cloud Container Registry to store customized container images that we may use during operating the framework. 40 | Google Cloud Storage is in use to store maintenance scripts and encrypted Gitlab backups. 41 | 42 | Automated deployment Pre-requisites: 43 | * Google Cloud subscription with **OWNER** permissions on a project (It is reccomended to use a **clean** GCP project). 44 | * Access to GCP cloud shell or locally using Terraform with GCP credentials. 45 | * Google Storage Bucket to store the backups. This Bucket is not required to share the same GCP project with the rest of the deployment. 46 | * A secret storing a password which will be used to encrypt/decrypt backup archives. 47 | 48 | 49 | 50 | Clone the repository 51 | ```bash 52 | git clone https://github.com/SygniaLabs/ScallOps.git 53 | cd ScallOps 54 | nano config.tfvars 55 | ``` 56 | 57 | Carefully read the instructions related to each variable and act accordingly. 58 | 59 | **project_id**, **infra_name**, **gitlab_backup_key_secret_id** and **backups_bucket_name** variables are required. 60 | 61 | The code below is an example for *config.tfvars* file using all available optional features: 62 | ```bash 63 | ##### Scallops IAC variables ##### 64 | #### Note that some variables are required (#Required), and some variables modifications will take effect also after deployment (#PostDeploymentModifiable). 65 | 66 | 67 | ## GCP Project ID 68 | project_id = "" #Required 69 | 70 | ## The name you wish to have as a prefix for the deployment's resources. Must comply with ^[a-z]([a-z0-9]*[a-z0-9])$ 71 | infra_name = "scallops" #Required 72 | 73 | ## The name of an existing bucket you wish to receive backups to. Terraform will create the required permission to upload the backup archive. 74 | backups_bucket_name = "" #Required #PostDeploymentModifiable 75 | 76 | ## An existing secret ID in the same GCP project (project_id) storing a password for the backup process (Allowed symbols for secret value: -_ ) 77 | ## Creating a secret through GCP secret manager https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets#create 78 | gitlab_backup_key_secret_id = "" #Required #PostDeploymentModifiable 79 | 80 | 81 | 82 | ## Gitlab version to install 83 | ## Ruuner chart version must be compaitibe with the Gitlab version -> https://docs.gitlab.com/runner/#gitlab-runner-versions 84 | ## Note the Gitlab application version from the selected Chart version -> https://artifacthub.io/packages/helm/gitlab/gitlab-runner 85 | ## You can make upgrades to your Gitlab instance from here. Just reset the instance once the `apply` completes. 86 | # gitlab_version = "16.1.2-ee" 87 | # runner_chart_url = "https://gitlab-charts.s3.amazonaws.com/gitlab-runner-0.54.0.tgz" 88 | 89 | 90 | ## IP addresses that can interact with the Gitlab instance via HTTP/S (Office IP / Home IPs) 91 | # operator_ips = [] #Optional #PostDeploymentModifiable 92 | 93 | ## Enable debugging resources such as IAP Firewall rules, and export of config files 94 | # debug_flag = false #Optional #PostDeploymentModifiable 95 | 96 | ## The Gitlab instance Web server protocol, http or https. 97 | # gitlab_instance_protocol = "https" #Optional 98 | 99 | ## Region for the k8s cluster, Gitlab instance and network. 100 | # region = us-central-1 #Optional 101 | 102 | ## Zone for the k8s cluster, Gitlab instance and network. 103 | # zone = "a" #Optional 104 | 105 | 106 | 107 | ## External DNS ## #Optional #PostDeploymentModifiable #GitlabRestartRequired 108 | ## Uncomment the 3 lines below if wishing to supply external DNS name for accessing Gitalb instance 109 | 110 | # dns_project_id = "" # The project ID where the managed DNS zone is located 111 | # dns_managed_zone_name = "mydomain-com" # The configured managed DNS zone name 112 | # external_hostname = "scallops.mydomain.com" # The hostname you wish to set of the instance (Must be subdomain of the managed zone) 113 | 114 | 115 | 116 | ## Docker hub credentials (https://docs.docker.com/docker-hub/access-tokens/) #Optional #PostDeploymentModifiable 117 | ## An existing secret name in secret-manager storing Dockerhub credentials to fetch private container images (format is username:password or username:access-token). 118 | # dockerhub-creds-secret = "" 119 | 120 | 121 | ## Scallops-Recipes repository. Use the default repository or specify alternative fork in a Git path HTTPS format. 122 | ## The specified repository will be imported to Gitlab as the Scallops-Recipes repository. 123 | ## *Ignored if performing a migration 124 | # scallops_recipes_git_url = "https://github.com/SygniaLabs/ScallOps-Recipes.git" #Optional 125 | # scallops_recipes_git_creds_secret = "my-github-creds-secret" #Optional 126 | 127 | 128 | 129 | ## Migration variables #Optional 130 | ## If you plan on migrating from a different gitlab instance, uncomment all migration variables below, and follow requirements. 131 | ## 1. 'gitlab_backup_key_secret_id' secret must store the password value decrypting the archived backup zip. 132 | ## 2. 'gitlab_version' must be equal to the version you are migrating from. 133 | ## 3. Operation requires Gsutil on the terraform deployer system as backup will be downloaded locally 134 | 135 | # migrate_gitlab = true ## If performing migration from another Gitlab instance and got a backup file from previous instance. true/false. 136 | # migrate_gitlab_backup_bucket = "" ## The Google Storage Bucket to your Gitlab backup e.g. 'mybucket1-abcd' 137 | # migrate_gitlab_backup_path = "" ## The path to the archived backup zip e.g 'backups/gitlab-xxx-backup.zip' 138 | 139 | ##### 140 | ``` 141 | 142 | 143 | If using locally, make sure that you are authenticated to your GCP 144 | ```bash 145 | gcloud auth list # List and check currently authenticated account 146 | gcloud auth login # Authneticate 147 | ``` 148 | 149 | 150 | Deploy using terraform while pointing to the created configuration file 151 | ```bash 152 | terraform init 153 | terraform apply --var-file=./config.tfvars 154 | ``` 155 | 156 | Once deployed, you should receive the Gitlab instance IP address and the secret name where the password of the Gitlab's root account is stored. 157 | Before accessing the Gitlab instance, you should look for 'Gitlab instance setup completed' in the Gcloud logging 'gitlab-startup' log name. 158 | Errors within Gitlab setup will also be logged, but not all of them will stop the setup. Checkout the entry above the error to understand which process errored. 159 | 160 | ```console 161 | .... 162 | .... 163 | module.gke.google_container_node_pool.pools["linux-pool"]: Still creating... [7m31s elapsed] 164 | module.gke.google_container_node_pool.pools["linux-pool"]: Creation complete after 7m32s [id=projects/my-scallops/locations/us-central1-a/clusters/scallops-offensive-pipeline/nodePools/linux-pool] 165 | module.gke_auth.data.google_container_cluster.gke_cluster: Reading... 166 | google_container_node_pool.windows-pool: Creating... 167 | module.gke_auth.data.google_container_cluster.gke_cluster: Read complete after 0s [id=projects/my-scallops/locations/us-central1-a/clusters/scallops-offensive-pipeline] 168 | kubernetes_namespace.sensitive-namespace: Creating... 169 | kubernetes_pod_disruption_budget.kube-dns: Creating... 170 | kubernetes_pod_disruption_budget.konnectivity-agent: Creating... 171 | kubernetes_secret.k8s_gitlab_cert_secret: Creating... 172 | helm_release.gitlab-runner-linux: Creating... 173 | kubernetes_pod_disruption_budget.konnectivity-agent: Creation complete after 1s [id=kube-system/k8s-pdb-konnectivity-agent] 174 | kubernetes_secret.k8s_gitlab_cert_secret: Creation complete after 1s [id=default/scallops-gitlab.us-central1-a.c.my-scallops.internal-cert] 175 | kubernetes_pod_disruption_budget.kube-dns: Creation complete after 1s [id=kube-system/k8s-pdb-kube-dns] 176 | kubernetes_namespace.sensitive-namespace: Creation complete after 1s [id=sensitive] 177 | kubernetes_secret.google-application-credentials: Creating... 178 | kubernetes_secret.dockerhub-creds-config[0]: Creating... 179 | kubernetes_secret.k8s_gitlab_cert_secret-sensitive: Creating... 180 | helm_release.gitlab-runner-win: Creating... 181 | kubernetes_secret.dockerhub-creds-config[0]: Creation complete after 0s [id=sensitive/dockerhub-creds-jsonconfig] 182 | kubernetes_secret.google-application-credentials: Creation complete after 0s [id=sensitive/kaniko-secret] 183 | kubernetes_secret.k8s_gitlab_cert_secret-sensitive: Creation complete after 0s [id=sensitive/scallops-gitlab.us-central1-a.c.my-scallops.internal-cert] 184 | helm_release.gitlab-runner-kaniko: Creating... 185 | helm_release.gitlab-runner-dockerhub[0]: Creating... 186 | google_container_node_pool.windows-pool: Still creating... [10s elapsed] 187 | helm_release.gitlab-runner-linux: Still creating... [10s elapsed] 188 | helm_release.gitlab-runner-win: Still creating... [10s elapsed] 189 | helm_release.gitlab-runner-kaniko: Still creating... [10s elapsed] 190 | helm_release.gitlab-runner-linux: Creation complete after 13s [id=linux] 191 | helm_release.gitlab-runner-win: Creation complete after 13s [id=windows] 192 | helm_release.gitlab-runner-dockerhub[0]: Still creating... [10s elapsed] 193 | helm_release.gitlab-runner-dockerhub[0]: Creation complete after 11s [id=dockerhub-privates] 194 | helm_release.gitlab-runner-kaniko: Creation complete after 12s [id=kaniko] 195 | google_container_node_pool.windows-pool: Still creating... [20s elapsed] 196 | google_container_node_pool.windows-pool: Still creating... [10m50s elapsed] 197 | google_container_node_pool.windows-pool: Creation complete after 10m55s [id=projects/my-scallops/locations/us-central1-a/clusters/scallops-offensive-pipeline/nodePools/windows-pool] 198 | 199 | Apply complete! Resources: 65 added, 0 changed, 0 destroyed. 200 | 201 | Outputs: 202 | 203 | gitlab_ext_ip = "X.X.X.X" 204 | gitlab_root_password_secret = "scallops-gitlab-root-password" 205 | ``` 206 | 207 | [ScallOps-Recipes](https://github.com/SygniaLabs/ScallOps-Recipes) repository should be pre-imported into the Gitlab insatnce together with few instance level CI/CD variables configured. All you have left to do is executing the recipes :) 208 | If you don't see the Recipes repository, you can import it [manually](https://docs.gitlab.com/ee/user/project/import/repo_by_url.html). 209 | > Important! If it is your initial deployment, you must run the deployment's initialization pipeline. 210 | 1. Refer to the imported Recipes repository at /ci/scallops-recipes in your Gitlab instance. 211 | 2. Navigate to CI/CD -> Pipelines -> Run Pipeline. 212 | 3. Delete all variables and add 'DEPLOYMENT_INIT' with value 1. 213 | 4. Hit 'Run Pipeline' and wait for the initialization process to complete. 214 | 215 | ## Architecture 216 | 217 | The included Terraform scripts will deploy the following resources into your GCP project: 218 | - Compute Engine - Gitlab Instance, managing sources and CI/CD jobs 219 | - Google Kubernetes Engine (GKE) - K8s cluster to host our CI/CD jobs 220 | - Default node pool - Single machine with K8s system pods and runners pods 221 | - Linux node pool - For the Gitlab-runners pods, and linux related jobs 222 | - Windows node pool - For Windows related jobs 223 | - Gitlab-runner Helm packages (Linux-runner, Windows-runner, Kaniko-runner, \[dockerhub-privates-runner\]) 224 | - Google Cloud Storage (GCS) - To store maintenance scripts 225 | - Google Artifact Registry - To store cutomized container images 226 | - Service Accounts 227 | 1. Used by Gitlab Compute instance to pull maintenance scripts and store backup 228 | 2. User who is attached to the GKE cluster nodes to pull container images from the project's registry (Read only) 229 | 3. User who is attached to the Kaniko CI job runner allowing to push customized container images to Artifact Registry 230 | - VPC network and related firewall rules to allow operation 231 | 232 |

233 | Infrastructure-layout 234 |

235 | 236 | 237 | 238 | ## GCP Cloud Costs 239 | 240 | Idle / Minimal usage: 241 | * Gitlab Instance: 51.46$ / month (n1-standard-2) 242 | * 2 x Linux node: 2 x 10.83$ = 21.66$ / month (e2-highcpu-2 preemptible) 243 | * Windows node: 134.34$ / month (t2d-standard-4 preemptible) 244 | * Storage 245 | * Utilities & Migration bucket: 20GB (depends on backup size) - 0.40$ / month 246 | * Artifact Registry (depends on container images volume): 100GB - 9.95$ / month 247 | * Boot Disks 248 | * Gitlab: Zonal SSD PD: 160 GB - 27.20$ / month 249 | * GKE Linux: Zonal SSD PD: 100 GB - 17$ / month 250 | * GKE Windows: Zonal SSD PD: 200 GB - 34$ / month 251 | * Secret manager: 0.06$ /month 252 | * GKE: One Zonal cluster is free per billing account 253 | * **Total: 318$ (us-central-1)** -- [source](https://cloud.google.com/products/calculator/#id=a582ea75-5872-4616-8313-41192c043ae7) 254 | 255 | Per Job: 256 | * Linux: Same as idle since system already up. When scaled 0.015$ per hour for each running node. 257 | * Windows: Same as idle since one system is already up. When scaled additional 0.2$ per hour for each running node. 258 | 259 | 260 | ## Backup procedure 261 | 262 | The deployment requires to supply a GCS bucket name to store the [Gitlab backup](https://docs.gitlab.com/ee/raketasks/backup_restore.html#back-up-gitlab). 263 | Backup occurs every Saturday during 10AM UTC, and is done by the Gitlab compute instance using the *[gitlab_backup.sh](scripts/bash/gitlab_backup.sh)* bash script, which is stored on GCS and setup into crontab during deployment. 264 | The backup archive is encrypted with a password stored in GCP secret-manager which is supplied (gitlab_backup_key_secret_id) by the user during Terraform deployment. 265 | 266 | The encrypted archive contains the following components: 267 | * Timestamp_yyyy_mm_dd_14.5.2-ee_gitlab_backup.tar (A file containing the Gitlab DB) 268 | * gitlab.rb (The main gitlab configuration file) 269 | * gitlab-secret.json (A file containing keys to decrypt various DB data) 270 | * ssl/hostname.key (SSL certificate private key belonging to the existing hostname) 271 | * ssl/hostname.cer (SSL public certificate file belonging to the existing hostname) 272 | 273 | The backup process log is casted to a [GCP log](https://cloud.google.com/logging/docs/view/logs-explorer-interface) named "gitlab-backup-exec". 274 | 275 | *Note that this backup structure works with the deployment migration capability. 276 | 277 | 278 | ## Open issues 279 | - Windows related containers are **not** built and deployed automatically to your Container Registry. For now, you will have to do it manually. You can use the supplied [Windows Dockerfiles](https://github.com/SygniaLabs/ScallOps-Recipes/tree/main/_ci-maintain/dockerfiles/windows). 280 | - Windows related containers must include native Powershell Core (PWSH). 281 | - Concurrent Linux related jobs are limited to 50 due to the Gitlab's instance compute resources. 282 | - Concurrent Windows related jobs are limited to 30 due to the time it takes to pull a container image when vertical scaling is triggered. 283 | 284 | 285 | ## Security Consideration 286 | Altough we made efforts to secure the given permissions and layout of the deployed environment, there may always be a scenario in which someone will achieve some sort of unautohrized access to the internal GCP cloud components. Therefore, it is **highly recommended to use a clean GCP project** when deploying this environment. 287 | 288 | ## References 289 | 290 | CI/CD guides: 291 | 292 | * CI/CD concept: https://hackernoon.com/understanding-the-basic-concepts-of-cicd-fw4k32s1 293 | * Gitlab CI docs: https://docs.gitlab.com/ee/ci/ 294 | * Gitlab CI Runner K8s executor: https://docs.gitlab.com/runner/executors/kubernetes.html 295 | 296 | Infrastructure references: 297 | 298 | * Terraform & Gcloud: https://registry.terraform.io/providers/hashicorp/google/latest/docs 299 | * GKE: https://cloud.google.com/kubernetes-engine/docs/concepts/kubernetes-engine-overview 300 | * Container registry access: https://cloud.google.com/container-registry/docs/access-control 301 | * Helm Charts: https://helm.sh/docs/topics/charts/ 302 | * Kaniko: https://github.com/GoogleContainerTools/kaniko -------------------------------------------------------------------------------- /config.tfvars: -------------------------------------------------------------------------------- 1 | ##### Scallops IAC variables ##### 2 | #### Note that some variables are required (#Required), and some variables modifications will take effect also after deployment (#PostDeploymentModifiable). 3 | 4 | 5 | ## GCP Project ID 6 | project_id = "" #Required 7 | 8 | ## The name you wish to have as a prefix for the deployment's resources. Must comply with ^[a-z]([a-z0-9]*[a-z0-9])$ 9 | infra_name = "scallops" #Required 10 | 11 | ## The name of an existing bucket you wish to receive backups to. Terraform will create the required permission to upload the backup archive. 12 | backups_bucket_name = "" #Required #PostDeploymentModifiable 13 | 14 | ## An existing secret ID in the same GCP project (project_id) storing a password for the backup process (Allowed symbols for secret value: -_ ) 15 | ## Creating a secret through GCP secret manager https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets#create 16 | gitlab_backup_key_secret_id = "" #Required #PostDeploymentModifiable 17 | 18 | 19 | 20 | ## Gitlab version to install 21 | ## Ruuner chart version must be compaitibe with the Gitlab version -> https://docs.gitlab.com/runner/#gitlab-runner-versions 22 | ## Note the Gitlab application version from the selected Chart version -> https://artifacthub.io/packages/helm/gitlab/gitlab-runner 23 | ## You can make upgrades to your Gitlab instance from here. Just reset the instance once the `apply` completes. 24 | # gitlab_version = "16.1.2-ee" 25 | # runner_chart_url = "https://gitlab-charts.s3.amazonaws.com/gitlab-runner-0.54.0.tgz" 26 | 27 | 28 | ## IP addresses that can interact with the Gitlab instance via HTTP/S (Office IP / Home IPs) 29 | # operator_ips = [] #Optional #PostDeploymentModifiable 30 | 31 | ## Enable debugging resources such as IAP Firewall rules, and export of config files 32 | # debug_flag = false #Optional #PostDeploymentModifiable 33 | 34 | ## The Gitlab instance Web server protocol, http or https. 35 | # gitlab_instance_protocol = "https" #Optional 36 | 37 | ## Region for the k8s cluster, Gitlab instance and network. 38 | # region = us-central-1 #Optional 39 | 40 | ## Zone for the k8s cluster, Gitlab instance and network. 41 | # zone = "a" #Optional 42 | 43 | 44 | 45 | ## External DNS ## #Optional #PostDeploymentModifiable #GitlabRestartRequired 46 | ## Uncomment the 3 lines below if wishing to supply external DNS name for accessing Gitalb instance 47 | 48 | # dns_project_id = "" # The project ID where the managed DNS zone is located 49 | # dns_managed_zone_name = "mydomain-com" # The configured managed DNS zone name 50 | # external_hostname = "scallops.mydomain.com" # The hostname you wish to set of the instance (Must be subdomain of the managed zone) 51 | 52 | 53 | 54 | ## Docker hub credentials (https://docs.docker.com/docker-hub/access-tokens/) #Optional #PostDeploymentModifiable 55 | ## An existing secret name in secret-manager storing Dockerhub credentials to fetch private container images (format is username:password or username:access-token). 56 | # dockerhub-creds-secret = "" 57 | 58 | 59 | ## Scallops-Recipes repository. Use the default repository or specify alternative fork in a Git path HTTPS format. 60 | ## The specified repository will be imported to Gitlab as the Scallops-Recipes repository. 61 | ## *Ignored if performing a migration 62 | # scallops_recipes_git_url = "https://github.com/SygniaLabs/ScallOps-Recipes.git" #Optional 63 | # scallops_recipes_git_creds_secret = "my-github-creds-secret" #Optional 64 | 65 | 66 | 67 | ## Migration variables #Optional 68 | ## If you plan on migrating from a different gitlab instance, uncomment all migration variables below, and follow requirements. 69 | ## 1. 'gitlab_backup_key_secret_id' secret must store the password value decrypting the archived backup zip. 70 | ## 2. 'gitlab_version' must be equal to the version you are migrating from. 71 | ## 3. Operation requires Gsutil on the terraform deployer system as backup will be downloaded locally 72 | 73 | # migrate_gitlab = true ## If performing migration from another Gitlab instance and got a backup file from previous instance. true/false. 74 | # migrate_gitlab_backup_bucket = "" ## The Google Storage Bucket to your Gitlab backup e.g. 'mybucket1-abcd' 75 | # migrate_gitlab_backup_path = "" ## The path to the archived backup zip e.g 'backups/gitlab-xxx-backup.zip' -------------------------------------------------------------------------------- /debug.tf: -------------------------------------------------------------------------------- 1 | ### Debugging resources #### 2 | 3 | # Identity Aware Proxy (IAP) 4 | # IAP is GCP feature used to connect to GCE over SSH/RDP with browser or IAP Desktop 5 | # Adding FW rule for connecting to linux nodes, windows nodes and Gitlab instance via IAP 6 | resource "google_compute_firewall" "iap_pipeline" { 7 | count = var.debug_flag ? 1 : 0 8 | name = "${var.infra_name}-allow-iap" 9 | network = module.gcp-network.network_name 10 | provider = google.offensive-pipeline 11 | allow { 12 | protocol = "tcp" 13 | ports = ["22","3389"] 14 | } 15 | 16 | source_ranges = ["35.235.240.0/20"] 17 | target_tags = [ 18 | local.gitlab_instance_name, 19 | local.gke_linux_pool_tag, 20 | local.gke_win_pool_tag 21 | ] 22 | } 23 | 24 | # DEBUG: Save Gitlab Server certificate. 25 | resource "local_file" "tls_key_pem_file" { 26 | count = var.debug_flag ? 1 : 0 27 | content = tls_private_key.gitlab-self-signed-cert-key.private_key_pem 28 | filename = "${path.module}/gitlab.local.key" 29 | } 30 | resource "local_file" "tls_cert_pem_file" { 31 | count = var.debug_flag ? 1 : 0 32 | content = tls_self_signed_cert.gitlab-self-signed-cert.cert_pem 33 | filename = "${path.module}/gitlab.local.crt" 34 | } 35 | 36 | # DEBUG: Save kube config file 37 | 38 | module "gke_auth" { 39 | count = var.debug_flag ? 1 : 0 40 | source = "terraform-google-modules/kubernetes-engine/google//modules/auth" 41 | project_id = var.project_id 42 | cluster_name = module.gke.name 43 | location = module.gke.location 44 | } 45 | 46 | resource "local_file" "kubeconfig" { 47 | depends_on = [module.gke_auth] 48 | count = var.debug_flag ? 1 : 0 49 | content = module.gke_auth[0].kubeconfig_raw 50 | filename = "${path.module}/kubeconfig" 51 | } 52 | -------------------------------------------------------------------------------- /gitlab-runner/dockerhub-values.yaml: -------------------------------------------------------------------------------- 1 | rbac: 2 | create: true 3 | rules: 4 | - apiGroups: [""] 5 | resources: ["pods/attach","pods/exec"] 6 | verbs: ["create", "patch", "delete"] 7 | - apiGroups: [""] 8 | resources: ["pods","services"] 9 | verbs: ["get","watch","create", "delete"] 10 | - apiGroups: [""] 11 | resources: ["configmaps","secrets"] 12 | verbs: ["get","create","update","delete"] 13 | 14 | runners: 15 | imagePullPolicy: always 16 | tags: "dockerhub,kubernetes" -------------------------------------------------------------------------------- /gitlab-runner/kaniko-values.yaml: -------------------------------------------------------------------------------- 1 | rbac: 2 | create: true 3 | rules: 4 | - apiGroups: [""] 5 | resources: ["pods/attach","pods/exec"] 6 | verbs: ["create", "patch", "delete"] 7 | - apiGroups: [""] 8 | resources: ["pods","services"] 9 | verbs: ["get","watch","create", "delete"] 10 | - apiGroups: [""] 11 | resources: ["configmaps","secrets"] 12 | verbs: ["get","create","update","delete"] 13 | 14 | runners: 15 | protected: true 16 | tags: "kaniko,kubernetes" -------------------------------------------------------------------------------- /gitlab-runner/linux-values.yaml: -------------------------------------------------------------------------------- 1 | rbac: 2 | create: true 3 | rules: 4 | - apiGroups: [""] 5 | resources: ["pods/attach","pods/exec"] 6 | verbs: ["create", "patch", "delete", "get"] 7 | - apiGroups: [""] 8 | resources: ["pods","services"] 9 | verbs: ["get","watch","create", "delete"] 10 | - apiGroups: [""] 11 | resources: ["configmaps","secrets"] 12 | verbs: ["get","create","update","delete"] 13 | 14 | runners: 15 | tags: "linux,kubernetes" -------------------------------------------------------------------------------- /gitlab-runner/win-values.yaml: -------------------------------------------------------------------------------- 1 | rbac: 2 | create: true 3 | rules: 4 | - apiGroups: [""] 5 | resources: ["pods/attach","pods/exec"] 6 | verbs: ["create", "patch", "delete"] 7 | - apiGroups: [""] 8 | resources: ["pods","services"] 9 | verbs: ["get","watch","create", "delete"] 10 | - apiGroups: [""] 11 | resources: ["configmaps","secrets"] 12 | verbs: ["get","create","update","delete"] 13 | 14 | runners: 15 | tags: "windows,kubernetes" -------------------------------------------------------------------------------- /gke.tf: -------------------------------------------------------------------------------- 1 | ########################### GKE Cluster ################################################# 2 | 3 | 4 | ## K8s secrets and namespaces 5 | resource "kubernetes_secret" "google-application-credentials" { 6 | data = { 7 | "kaniko-token-secret.json" = base64decode(google_service_account_key.artifact_registry_writer.private_key) 8 | } 9 | metadata { 10 | name = "kaniko-secret" 11 | namespace = kubernetes_namespace.sensitive-namespace.id 12 | } 13 | } 14 | 15 | resource "kubernetes_secret" "k8s_gitlab_cert_secret-sensitive" { 16 | data = { 17 | "${local.instance_internal_domain}.crt" = tls_self_signed_cert.gitlab-self-signed-cert.cert_pem 18 | } 19 | metadata { 20 | name = "${local.instance_internal_domain}-cert" 21 | namespace = kubernetes_namespace.sensitive-namespace.id 22 | } 23 | } 24 | 25 | resource "kubernetes_secret" "dockerhub-creds-config" { 26 | count = var.dockerhub-creds-secret != "" ? 1 : 0 27 | data = { 28 | ".dockerconfigjson" = jsonencode({ 29 | auths = { 30 | "https://index.docker.io/v1/" = { 31 | auth = "${base64encode("${data.google_secret_manager_secret_version.dockerhub-secret[0].secret_data}")}" 32 | } 33 | } 34 | }) 35 | } 36 | type = "kubernetes.io/dockerconfigjson" 37 | metadata { 38 | name = "dockerhub-creds-jsonconfig" 39 | namespace = kubernetes_namespace.sensitive-namespace.id 40 | } 41 | } 42 | 43 | 44 | resource "kubernetes_namespace" "sensitive-namespace" { 45 | depends_on = [module.gke.google_container_node_pool] 46 | metadata { 47 | annotations = {name = "Store Kaniko and Dockerhub creds secrets and their related pod runners"} 48 | name = "sensitive" 49 | } 50 | } 51 | 52 | resource "kubernetes_secret" "k8s_gitlab_cert_secret" { 53 | depends_on = [module.gke.google_container_node_pool] 54 | data = { 55 | "${local.instance_internal_domain}.crt" = tls_self_signed_cert.gitlab-self-signed-cert.cert_pem 56 | } 57 | metadata { 58 | name = "${local.instance_internal_domain}-cert" 59 | namespace = "default" 60 | } 61 | } 62 | 63 | 64 | # Pod disruption budget 65 | 66 | resource "kubernetes_pod_disruption_budget_v1" "kube-dns" { 67 | depends_on = [module.gke.google_container_node_pool] 68 | metadata { 69 | name = "k8s-pdb-kube-dns" 70 | namespace = "kube-system" 71 | } 72 | spec { 73 | max_unavailable = 1 74 | selector { 75 | match_labels = { 76 | k8s-app = "kube-dns" 77 | } 78 | } 79 | } 80 | } 81 | 82 | 83 | resource "kubernetes_pod_disruption_budget_v1" "konnectivity-agent" { 84 | depends_on = [module.gke.google_container_node_pool] 85 | metadata { 86 | name = "k8s-pdb-konnectivity-agent" 87 | namespace = "kube-system" 88 | } 89 | spec { 90 | max_unavailable = "50%" 91 | selector { 92 | match_labels = { 93 | k8s-app = "konnectivity-agent" 94 | } 95 | } 96 | } 97 | } 98 | 99 | 100 | ## K8s cluster 101 | 102 | module "gke" { 103 | source = "terraform-google-modules/kubernetes-engine/google" 104 | version = "26.1.1" # https://github.com/terraform-google-modules/terraform-google-kubernetes-engine 105 | kubernetes_version = var.gke_version 106 | project_id = var.project_id 107 | name = "${var.infra_name}-offensive-pipeline" 108 | regional = false 109 | region = var.region #Required if Regional true 110 | zones = ["${var.region}-${var.zone}"] 111 | network = module.gcp-network.network_name 112 | subnetwork = module.gcp-network.subnets_names[0] 113 | default_max_pods_per_node = 30 114 | ip_range_pods = "${var.infra_name}-gke-pods-subnet" 115 | ip_range_services = "${var.infra_name}-gke-service-subnet" 116 | http_load_balancing = false 117 | horizontal_pod_autoscaling = true 118 | network_policy = false 119 | # remove_default_node_pool = true 120 | initial_node_count = 1 121 | create_service_account = true 122 | grant_registry_access = true 123 | 124 | node_pools = [ 125 | { 126 | name = "linux-pool" 127 | version = var.gke_linux_pool_version 128 | machine_type = "e2-highcpu-2" 129 | min_count = 1 130 | max_count = 8 131 | local_ssd_count = 0 132 | disk_size_gb = 100 133 | disk_type = "pd-ssd" 134 | image_type = "COS_CONTAINERD" 135 | enable_gcfs = true 136 | auto_repair = true 137 | auto_upgrade = true 138 | spot = true 139 | initial_node_count = 1 140 | } 141 | ] 142 | 143 | node_pools_oauth_scopes = { 144 | linux-pool = ["https://www.googleapis.com/auth/cloud-platform"] 145 | } 146 | 147 | node_pools_labels = { 148 | all = {} 149 | } 150 | node_pools_metadata = { 151 | all = { 152 | disable-legacy-endpoints = true 153 | } 154 | } 155 | node_pools_taints = { 156 | all = [] 157 | } 158 | node_pools_tags = { 159 | linux-pool = [local.gke_linux_pool_tag] 160 | } 161 | } 162 | 163 | 164 | 165 | ## K8s windows node pool 166 | 167 | resource "google_container_node_pool" "windows-pool" { 168 | cluster = module.gke.cluster_id 169 | initial_node_count = 1 170 | location = "${var.region}-${var.zone}" 171 | max_pods_per_node = 8 172 | name = "windows-pool" 173 | #node_count = 0 174 | node_locations = ["${var.region}-${var.zone}"] 175 | provider = google.offensive-pipeline 176 | version = var.gke_windows_pool_version 177 | autoscaling { 178 | max_node_count = 8 179 | min_node_count = 1 # at least 1 required since Gitlab K8s runner for windows has issue with scaling-up from 0->1 nodes as node-pool label cant contain the windows build version 180 | } 181 | 182 | management { 183 | auto_repair = true 184 | auto_upgrade = false 185 | } 186 | 187 | node_config { 188 | disk_size_gb = 200 189 | disk_type = "pd-ssd" 190 | guest_accelerator = [] 191 | image_type = "windows_ltsc_containerd" 192 | labels = { 193 | "cluster_name" = module.gke.name 194 | "node_pool" = "windows-pool" 195 | } 196 | local_ssd_count = 0 197 | machine_type = "t2d-standard-4" 198 | metadata = { 199 | "cluster_name" = module.gke.name 200 | "disable-legacy-endpoints" = "true" 201 | "node_pool" = "windows-pool" 202 | "windows" = "true" 203 | "windows-startup-script-url" = local.gke_win_pool_start_script 204 | } 205 | oauth_scopes = ["https://www.googleapis.com/auth/cloud-platform"] 206 | preemptible = true 207 | service_account = module.gke.service_account 208 | tags = [local.gke_win_pool_tag] 209 | taint = [ 210 | { 211 | effect = "PREFER_NO_SCHEDULE" 212 | key = "node.kubernetes.io/os" 213 | value = "windows" 214 | }, 215 | { 216 | effect = "NO_SCHEDULE" 217 | key = "node.kubernetes.io/os" 218 | value = "windows" 219 | } 220 | ] 221 | 222 | shielded_instance_config { 223 | enable_integrity_monitoring = true 224 | enable_secure_boot = false 225 | } 226 | 227 | workload_metadata_config { 228 | mode = "GKE_METADATA" 229 | } 230 | } 231 | 232 | timeouts { 233 | create = "45m" 234 | delete = "45m" 235 | update = "45m" 236 | } 237 | 238 | upgrade_settings { 239 | max_surge = 2 240 | max_unavailable = 0 241 | } 242 | } 243 | 244 | 245 | ### Artifact Registry Repository ### 246 | 247 | resource "google_artifact_registry_repository" "containers" { 248 | location = var.region 249 | repository_id = local.artifact_registry_id 250 | description = "Container repository created by terraform for ${var.infra_name}" 251 | format = "DOCKER" 252 | provider = google.offensive-pipeline 253 | } -------------------------------------------------------------------------------- /iam.tf: -------------------------------------------------------------------------------- 1 | #### IAM svc accounts, roles and bindings #### 2 | 3 | ## Service Accounts ## 4 | 5 | # Gitlab compute instance service account 6 | resource "google_service_account" "gitlab_service_account" { 7 | account_id = "${local.gitlab_instance_name}-svc" 8 | display_name = "Gitlab Service Account" 9 | provider = google.offensive-pipeline 10 | } 11 | 12 | # Service account with capability to push container images to sepcific repository in artifact registry 13 | resource "google_service_account" "kaniko" { 14 | account_id = "${var.infra_name}-gke-bucket" 15 | display_name = "Service Account for Pods to access artifact registry repository" 16 | provider = google.offensive-pipeline 17 | } 18 | 19 | ## Roles ## 20 | 21 | resource "random_string" "custom_roles_suffix" { 22 | length = 4 23 | special = false 24 | lower = true 25 | upper = false 26 | } 27 | 28 | # Create role with permissions to backup only without read/overwrite/delete 29 | resource "google_project_iam_custom_role" "backup_archive_role" { 30 | role_id = "gitlab_backupArchive_${var.infra_name}_${random_string.custom_roles_suffix.result}" 31 | title = "Gitlab Backup Role for ${var.infra_name}" 32 | description = "A role attached to the gitlab compute service account allowing it to update new backup archives to the specified bucket (var.backups_bucket_name)." 33 | permissions = [ 34 | "storage.objects.list", 35 | "storage.objects.create", 36 | "storage.multipartUploads.create", 37 | "storage.multipartUploads.listParts", 38 | "storage.multipartUploads.abort" 39 | ] 40 | provider = google.offensive-pipeline 41 | } 42 | 43 | 44 | # Role that allows startup script write logs 45 | resource "google_project_iam_custom_role" "compute_log_role" { 46 | role_id = "gitlab_customRole_${var.infra_name}_${random_string.custom_roles_suffix.result}" 47 | title = "Write logs role for ${var.infra_name}" 48 | description = "A role attached to the gitlab compute instance allowing it to write logs." 49 | permissions = ["logging.logEntries.create"] 50 | provider = google.offensive-pipeline 51 | } 52 | 53 | 54 | ## Bindings ## 55 | 56 | # Service account user role binding 57 | resource "google_project_iam_member" "sa_binding" { 58 | project = var.project_id 59 | provider = google.offensive-pipeline 60 | role = "roles/iam.serviceAccountUser" 61 | member = "serviceAccount:${google_service_account.gitlab_service_account.email}" 62 | } 63 | 64 | # Gitlab instance IAM Binding to storage 65 | resource "google_storage_bucket_iam_binding" "binding" { 66 | bucket = google_storage_bucket.deployment_utils.name 67 | role = "roles/storage.objectViewer" # To Read startup scripts on deployment utils bucket. 68 | members = [ 69 | "serviceAccount:${google_service_account.gitlab_service_account.email}" 70 | ] 71 | } 72 | 73 | 74 | # Attach gitlab compute service account to the bucket with the backup role 75 | resource "google_storage_bucket_iam_binding" "backup_bucket_binding" { 76 | bucket = var.backups_bucket_name 77 | role = google_project_iam_custom_role.backup_archive_role.name 78 | members = [ 79 | "serviceAccount:${google_service_account.gitlab_service_account.email}" 80 | ] 81 | } 82 | 83 | # Bind the compute custom role the the service account 84 | resource "google_project_iam_binding" "compute_binding" { 85 | project = var.project_id 86 | provider = google.offensive-pipeline 87 | role = google_project_iam_custom_role.compute_log_role.name 88 | members = [ 89 | "serviceAccount:${google_service_account.gitlab_service_account.email}", 90 | ] 91 | } 92 | 93 | 94 | # Bind artifact registry write access to specific repository 95 | resource "google_artifact_registry_repository_iam_member" "writer" { 96 | project = google_artifact_registry_repository.containers.project 97 | location = google_artifact_registry_repository.containers.location 98 | repository = google_artifact_registry_repository.containers.name 99 | role = "roles/artifactregistry.writer" 100 | member = "serviceAccount:${google_service_account.kaniko.email}" 101 | } 102 | 103 | resource "google_service_account_key" "artifact_registry_writer" { 104 | service_account_id = google_service_account.kaniko.name 105 | } 106 | 107 | 108 | # IAM Bindings for Gitlab instance to secrets 109 | resource "google_secret_manager_secret_iam_binding" "gitlab-self-key-binding" { 110 | project = google_secret_manager_secret.gitlab-self-signed-cert-key.project 111 | secret_id = google_secret_manager_secret.gitlab-self-signed-cert-key.secret_id 112 | role = "roles/secretmanager.secretAccessor" 113 | members = [ 114 | "serviceAccount:${google_service_account.gitlab_service_account.email}", 115 | ] 116 | } 117 | 118 | resource "google_secret_manager_secret_iam_binding" "gitlab-self-crt-binding" { 119 | project = google_secret_manager_secret.gitlab-self-signed-cert-crt.project 120 | secret_id = google_secret_manager_secret.gitlab-self-signed-cert-crt.secret_id 121 | role = "roles/secretmanager.secretAccessor" 122 | members = [ 123 | "serviceAccount:${google_service_account.gitlab_service_account.email}", 124 | ] 125 | } 126 | 127 | 128 | resource "google_secret_manager_secret_iam_binding" "gitlab_runner_registration_token" { 129 | project = google_secret_manager_secret.gitlab_runner_registration_token.project 130 | secret_id = google_secret_manager_secret.gitlab_runner_registration_token.secret_id 131 | role = "roles/secretmanager.secretAccessor" 132 | members = [ 133 | "serviceAccount:${google_service_account.gitlab_service_account.email}", 134 | ] 135 | } 136 | 137 | 138 | resource "google_secret_manager_secret_iam_binding" "gitlab_initial_root_pwd" { 139 | project = google_secret_manager_secret.gitlab_initial_root_pwd.project 140 | secret_id = google_secret_manager_secret.gitlab_initial_root_pwd.secret_id 141 | role = "roles/secretmanager.secretAccessor" 142 | members = [ 143 | "serviceAccount:${google_service_account.gitlab_service_account.email}", 144 | ] 145 | } 146 | 147 | 148 | resource "google_secret_manager_secret_iam_member" "gitlab_backup_key" { 149 | project = var.project_id 150 | secret_id = var.gitlab_backup_key_secret_id 151 | role = "roles/secretmanager.secretAccessor" 152 | member = "serviceAccount:${google_service_account.gitlab_service_account.email}" 153 | } 154 | 155 | resource "google_secret_manager_secret_iam_member" "git_creds" { 156 | count = var.scallops_recipes_git_creds_secret != "" ? 1 : 0 157 | project = var.project_id 158 | secret_id = var.scallops_recipes_git_creds_secret 159 | role = "roles/secretmanager.secretAccessor" 160 | member = "serviceAccount:${google_service_account.gitlab_service_account.email}" 161 | } -------------------------------------------------------------------------------- /img/scallops-infra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SygniaLabs/ScallOps/de953f69600132ab5b607c52cdaaed6d60de8751/img/scallops-infra.png -------------------------------------------------------------------------------- /img/scallops.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SygniaLabs/ScallOps/de953f69600132ab5b607c52cdaaed6d60de8751/img/scallops.png -------------------------------------------------------------------------------- /load-balancer.tf: -------------------------------------------------------------------------------- 1 | resource "google_compute_managed_ssl_certificate" "lb_proxy_ssl_cert" { 2 | provider = google.offensive-pipeline 3 | count = var.external_hostname != "" ? 1 : 0 4 | name = "${local.gitlab_instance_name}-ssl-cert" 5 | managed { 6 | domains = [var.external_hostname] 7 | } 8 | } 9 | 10 | resource "google_compute_health_check" "gitlab_service_hc" { 11 | provider = google.offensive-pipeline 12 | name = "${local.gitlab_instance_name}-backend-hc" 13 | timeout_sec = 5 14 | check_interval_sec = 30 15 | healthy_threshold = 1 16 | unhealthy_threshold = 3 17 | 18 | dynamic "http_health_check" { 19 | for_each = var.gitlab_instance_protocol == "http" ? [1] : [] 20 | content { 21 | port_name = "http" 22 | port_specification = "USE_NAMED_PORT" 23 | request_path = "/robots.txt" 24 | proxy_header = "NONE" 25 | } 26 | } 27 | 28 | dynamic "https_health_check" { 29 | for_each = var.gitlab_instance_protocol == "https" ? [1] : [] 30 | content { 31 | port_name = "https" 32 | port_specification = "USE_NAMED_PORT" 33 | request_path = "/robots.txt" 34 | proxy_header = "NONE" 35 | } 36 | } 37 | 38 | } 39 | 40 | resource "google_compute_security_policy" "lb_allow_operators_policy" { 41 | provider = google.offensive-pipeline 42 | name = "${local.gitlab_instance_name}-backend-allowed-ips" 43 | 44 | 45 | 46 | rule { 47 | action = "allow" 48 | priority = "1000" 49 | description = "Allow specific IPs" 50 | match { 51 | versioned_expr = "SRC_IPS_V1" 52 | config { 53 | src_ip_ranges = var.operator_ips 54 | } 55 | } 56 | 57 | } 58 | 59 | # Conditionally create rule for external integration address ranges 60 | dynamic "rule" { 61 | for_each = var.external_integration_ranges != null ? [1] : [] 62 | content { 63 | action = "allow" 64 | priority = 1001 65 | description = "External Integration IP addresses" 66 | 67 | match { 68 | versioned_expr = "SRC_IPS_V1" 69 | config { 70 | src_ip_ranges = var.external_integration_ranges 71 | } 72 | } 73 | } 74 | } 75 | 76 | 77 | rule { 78 | action = "deny(502)" 79 | priority = "2147483647" 80 | description = "Default rule deny all addresses" 81 | match { 82 | versioned_expr = "SRC_IPS_V1" 83 | config { 84 | src_ip_ranges = ["*"] 85 | } 86 | } 87 | 88 | } 89 | } 90 | 91 | resource "google_compute_backend_service" "gitlab_instance_service" { 92 | provider = google.offensive-pipeline 93 | name = "${local.gitlab_instance_name}-backend" 94 | port_name = var.gitlab_instance_protocol 95 | protocol = upper(var.gitlab_instance_protocol) 96 | timeout_sec = 10 97 | security_policy = google_compute_security_policy.lb_allow_operators_policy.self_link 98 | backend { 99 | group = google_compute_instance_group.gitlab_instance.self_link 100 | } 101 | health_checks = [ 102 | google_compute_health_check.gitlab_service_hc.self_link, 103 | ] 104 | } 105 | 106 | resource "google_compute_url_map" "service_url_map" { 107 | provider = google.offensive-pipeline 108 | name = "${local.gitlab_instance_name}-urlmap" 109 | default_service = google_compute_backend_service.gitlab_instance_service.self_link 110 | } 111 | 112 | resource "google_compute_target_https_proxy" "lb_https_proxy" { 113 | provider = google.offensive-pipeline 114 | name = "${local.gitlab_instance_name}-https-proxy" 115 | url_map = google_compute_url_map.service_url_map.self_link 116 | ssl_certificates = ( 117 | var.external_hostname != "" ? 118 | [google_compute_managed_ssl_certificate.lb_proxy_ssl_cert.0.self_link] : [] 119 | ) 120 | } 121 | 122 | resource "google_compute_global_forwarding_rule" "lb_forward_rule" { 123 | provider = google.offensive-pipeline 124 | name = "${local.gitlab_instance_name}-fwd-rule" 125 | target = google_compute_target_https_proxy.lb_https_proxy.self_link 126 | port_range = "443" 127 | } 128 | 129 | # https://cloud.google.com/load-balancing/docs/firewall-rules 130 | resource "google_compute_firewall" "allow_lb_gitlab_access" { 131 | provider = google.offensive-pipeline 132 | name = "${local.gitlab_instance_name}-allow-lb" 133 | network = module.gcp-network.network_name 134 | source_ranges = ["130.211.0.0/22", "35.191.0.0/16"] 135 | target_tags = google_compute_instance.gitlab.tags 136 | allow { 137 | protocol = "tcp" 138 | ports = var.operator_ports 139 | } 140 | } -------------------------------------------------------------------------------- /locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | gitlab_instance_name = "${var.infra_name}-gitlab" 3 | instance_internal_domain = "${local.gitlab_instance_name}.${var.region}-${var.zone}.c.${var.project_id}.internal" 4 | instance_internal_url = "${var.gitlab_instance_protocol}://${local.instance_internal_domain}" 5 | gitlab_package_dl_link = join("/", [ 6 | "https://packages.gitlab.com/gitlab/gitlab-ee/packages", 7 | "${var.os_name}", 8 | "${var.os_release}", 9 | "gitlab-ee_${var.gitlab_version}.0_amd64.deb", 10 | "download.deb" 11 | ]) 12 | vpc_main_subnet = "10.0.0.0/22" # 10.0.0.0 - 10.0.3.255 , 1024 IPs 13 | gke_pod_subnet = "10.2.0.0/17" # 10.2.0.0 - 10.2.127.255 - 32,768 IPs. 14 | gke_svc_subnet = "10.2.128.0/20" # 10.2.128.0 - 10.2.143.255 - 4096 IPs. 15 | artifact_registry_host = "${var.region}-docker.pkg.dev" 16 | artifact_registry_id = "${var.infra_name}-containers" 17 | artifact_registry_namespace = "${var.project_id}/${local.artifact_registry_id}" 18 | gke_linux_pool_tag = "gke-${var.infra_name}-offensive-pipeline-gke-linux-pool" 19 | gke_win_pool_tag = "gke-${var.infra_name}-offensive-pipeline-windows-pool" 20 | gke_win_pool_start_script = join("/", [ 21 | "gs:/", 22 | google_storage_bucket.deployment_utils.name, 23 | google_storage_bucket_object.disable_windows_defender_ps.name 24 | ]) 25 | 26 | gitlab_startup_script = join("/", [ 27 | "gs:/", 28 | google_storage_bucket.deployment_utils.name, 29 | google_storage_bucket_object.gitlab_startup_script.name 30 | ]) 31 | 32 | gitlab_migrate_backup = var.migrate_gitlab ? join("/", [ 33 | "gs:/", 34 | google_storage_bucket.deployment_utils.name, 35 | var.migrate_gitlab_backup_path 36 | ]) : "" 37 | } -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- 1 | ########################### Gitlab Instance ################################################# 2 | 3 | resource "google_compute_instance" "gitlab" { 4 | depends_on = [ 5 | # If migrating we have to wait for the backup to upload 6 | null_resource.transfer_gitlab_backup[0], 7 | # Need to wait for the secrets' values to take place in the secrets objects 8 | google_secret_manager_secret_version.gitlab-self-signed-cert-crt-version, 9 | google_secret_manager_secret_version.gitlab-self-signed-cert-key-version, 10 | google_secret_manager_secret_version.gitlab_initial_root_pwd, 11 | google_secret_manager_secret_version.gitlab_runner_registration_token, 12 | # Compute instance doesn't wait for any binding related to the service account to complete. These are required for the startup scripts 13 | google_storage_bucket_iam_binding.binding, 14 | google_project_iam_binding.compute_binding, 15 | google_storage_bucket_iam_binding.backup_bucket_binding, 16 | google_secret_manager_secret_iam_binding.gitlab-self-key-binding, 17 | google_secret_manager_secret_iam_binding.gitlab-self-crt-binding, 18 | google_secret_manager_secret_iam_binding.gitlab_runner_registration_token, 19 | google_secret_manager_secret_iam_binding.gitlab_initial_root_pwd, 20 | google_secret_manager_secret_iam_member.gitlab_backup_key, 21 | google_secret_manager_secret_iam_member.git_creds 22 | ] 23 | provider = google.offensive-pipeline 24 | name = local.gitlab_instance_name 25 | machine_type = var.plans[var.size] 26 | zone = "${var.region}-${var.zone}" 27 | tags = [local.gitlab_instance_name] 28 | 29 | 30 | 31 | service_account { 32 | email = google_service_account.gitlab_service_account.email 33 | scopes = ["cloud-platform"] 34 | } 35 | 36 | boot_disk { 37 | initialize_params { 38 | image = var.os_images[var.os_name][var.os_release] 39 | size = "160" 40 | type = "pd-ssd" 41 | } 42 | } 43 | 44 | network_interface { 45 | subnetwork = module.gcp-network.subnets_self_links[0] 46 | access_config {} 47 | } 48 | 49 | 50 | metadata = { 51 | gcs-prefix = "gs://${google_storage_bucket.deployment_utils.name}" 52 | gcs-path-to-backup = var.migrate_gitlab ? local.gitlab_migrate_backup : "NONE" # Migration var 53 | startup-script-url = local.gitlab_startup_script 54 | instance-external-domain = var.external_hostname != "" ? var.external_hostname : local.instance_internal_domain 55 | instance-protocol = var.gitlab_instance_protocol 56 | gitlab-initial-root-pwd-secret = google_secret_manager_secret.gitlab_initial_root_pwd.secret_id 57 | gitlab-cert-key-secret = google_secret_manager_secret.gitlab-self-signed-cert-key.secret_id 58 | gitlab-cert-public-secret = google_secret_manager_secret.gitlab-self-signed-cert-crt.secret_id 59 | gitlab-ci-runner-registration-token-secret = google_secret_manager_secret.gitlab_runner_registration_token.secret_id 60 | gitlab-backup-key-secret = var.gitlab_backup_key_secret_id 61 | gitlab-backup-bucket-name = var.backups_bucket_name 62 | target-gitlab-version = var.gitlab_version 63 | gitlab-package-dl-link = local.gitlab_package_dl_link 64 | container-registry-host = local.artifact_registry_host 65 | container-registry-namespace = local.artifact_registry_namespace 66 | scallops-recipes-git-url = var.scallops_recipes_git_url 67 | scallops-recipes-git-creds-secret = var.scallops_recipes_git_creds_secret != "" ? var.scallops_recipes_git_creds_secret : "NONE" 68 | } 69 | } 70 | 71 | 72 | resource "google_compute_instance_group" "gitlab_instance" { 73 | provider = google.offensive-pipeline 74 | name = "${local.gitlab_instance_name}-grp" 75 | description = "Instance group for Gitlab" 76 | zone = google_compute_instance.gitlab.zone 77 | instances = [google_compute_instance.gitlab.self_link] 78 | 79 | named_port { 80 | name = "https" 81 | port = 443 82 | } 83 | named_port { 84 | name = "http" 85 | port = 80 86 | } 87 | } 88 | 89 | 90 | 91 | ################################ Helm chart deployments ############################## 92 | 93 | 94 | resource "helm_release" "gitlab-runner-linux" { 95 | depends_on = [module.gke.google_container_node_pool] 96 | name = "linux" 97 | wait = false 98 | chart = var.runner_chart_url 99 | 100 | values = [ 101 | file("${path.module}/gitlab-runner/linux-values.yaml") 102 | ] 103 | 104 | set { 105 | name = "gitlabUrl" 106 | value = local.instance_internal_url 107 | } 108 | set { 109 | name = "certsSecretName" 110 | value = "${local.instance_internal_domain}-cert" 111 | } 112 | set_sensitive { 113 | name = "runnerRegistrationToken" 114 | value = random_password.gitlab_runner_registration_token.result 115 | } 116 | set { 117 | name = "sessionServer.enabled" 118 | value = "true" 119 | } 120 | set { 121 | name = "sessionServer.timeout" 122 | value = 1800 123 | } 124 | set { 125 | name = "sessionServer.loadBalancerSourceRanges[0]" 126 | value = "${google_compute_instance.gitlab.network_interface.0.access_config.0.nat_ip}/32" 127 | } 128 | set { 129 | name = "runners.config" 130 | value = <$STD_OUT_PATH 2>$STD_ERR_PATH 43 | 44 | local errCode=$? 45 | if [ $errCode -ne 0 ]; then 46 | errMsg=$(cat $STD_ERR_PATH) 47 | logger $logName "ERROR" "ErrCode: $errCode, ErrAction: $errAction, Message: $errMsg" 48 | if [[ $errAction == $ERR_ACTION_EXIT ]]; then 49 | logger $logName "DEBUG" "Stopping execution due to error action" 50 | exit 1 51 | fi 52 | fi 53 | cat $STD_OUT_PATH 54 | } 55 | 56 | exec_wrapper_no_print () { 57 | local errAction=$1 58 | local logName=$2 59 | local cmdExec=$3 60 | echo "wrapper exec: no print." 61 | $cmdExec 2> $STD_ERR_PATH 62 | 63 | local errCode=$? 64 | if [ $errCode -ne 0 ]; then 65 | errMsg=$(cat $STD_ERR_PATH) 66 | logger $logName "ERROR" "ErrCode: $errCode, ErrAction: $errAction, Message: $errMsg" 67 | if [[ $errAction == $ERR_ACTION_EXIT ]]; then 68 | logger $logName "DEBUG" "Stopping execution due to error action" 69 | exit 1 70 | fi 71 | fi 72 | } -------------------------------------------------------------------------------- /scripts/bash/gitlab_backup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin" 4 | 5 | #Imports 6 | DEPLOYMENT_GCS_PREFIX=`curl -H "Metadata-Flavor: Google" http://169.254.169.254/computeMetadata/v1/instance/attributes/gcs-prefix` 7 | GCLOUD_LOG_NAME="gitlab-backup-exec" 8 | 9 | gsutil cp $DEPLOYMENT_GCS_PREFIX/scripts/bash/gcloud_logger.sh ./ 10 | source ./gcloud_logger.sh 11 | gsutil cp $DEPLOYMENT_GCS_PREFIX/scripts/bash/gitlab_helpers.sh ./ 12 | source ./gitlab_helpers.sh 13 | 14 | 15 | 16 | check_installation $GCLOUD_LOG_NAME 17 | 18 | if [ $GITLAB_INSTALLED == 'true' ]; then 19 | logger $GCLOUD_LOG_NAME "INFO" "Performing gitlab backup" 20 | get_backup_archive_password $GCLOUD_LOG_NAME 21 | execute_backup $GCLOUD_LOG_NAME $GITLAB_EE_VERSION 22 | 23 | else 24 | logger $GCLOUD_LOG_NAME "ERROR" "Gitlab installation was not found!" 25 | fi 26 | -------------------------------------------------------------------------------- /scripts/bash/gitlab_helpers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | readonly RAILS_CMD_PATH=$(pwd)/railscmd.rb 4 | 5 | check_installation() { 6 | local logName=$1 7 | 8 | # In case GitLab instance gets restarted, skip the script. 9 | logger $logName "INFO" "Checking whether GitLab is installed" 10 | GITLAB_VERSION_FILE=/opt/gitlab/version-manifest.txt 11 | GITLAB_INSTALLED="false" 12 | 13 | if [ -f "$GITLAB_VERSION_FILE" ]; then 14 | logger $logName "DEBUG" "$GITLAB_VERSION_FILE exists" 15 | GITLAB_INSTALLED="true" 16 | GITLAB_EE_VERSION=$(grep gitlab-ee $GITLAB_VERSION_FILE | cut -d " " -f2)-ee 17 | logger $logName "INFO" "Installed GitLab version is $GITLAB_EE_VERSION" 18 | else 19 | logger $logName "DEBUG" "$GITLAB_VERSION_FILE does not exist" 20 | fi 21 | } 22 | 23 | check_upgrade() { 24 | local logName=$1 25 | local gitlabTargetVersion=$2 26 | 27 | # Check if we wish to upgrade the Gitlab application 28 | # Current GITLAB_EE_VERSION is required in the env. 29 | logger $logName "INFO" "Checking whether we want to upgrade Gitlab" 30 | if [ "$GITLAB_EE_VERSION" != "$gitlabTargetVersion" ]; then 31 | logger $logName "INFO" "GitLab version $GITLAB_EE_VERSION does not match the target version $gitlabTargetVersion. Upgrading..." 32 | # Skip the auto backup when making an upgrade 33 | logger $logName "INFO" "Skipping auto backup" 34 | exec_wrapper $ERR_ACTION_CONT $logName "touch /etc/gitlab/skip-auto-backup" 35 | 36 | logger $logName "INFO" "Updating package soruces..." 37 | exec_wrapper $ERR_ACTION_EXIT $logName "apt update" 38 | exec_wrapper $ERR_ACTION_CONT $logName "apt-cache madison gitlab-ee" 39 | 40 | logger $logName "INFO" "Installing Gitlab at version $gitlabTargetVersion" 41 | exec_wrapper $ERR_ACTION_EXIT $logName "apt install gitlab-ee=$gitlabTargetVersion.0" 42 | 43 | logger $logName "INFO" "Running checks..." 44 | exec_wrapper $ERR_ACTION_CONT $logName "gitlab-ctl status" 45 | exec_wrapper $ERR_ACTION_CONT $logName "gitlab-rake gitlab:check SANITIZE=true" 46 | 47 | logger $logName "INFO" "GitLab version upgraded successfully" 48 | else 49 | logger $logName "INFO" "GitLab is aligned with the targeted version" 50 | fi 51 | } 52 | 53 | 54 | gitlab_deps_install () { 55 | local logName=$1 56 | # Install Dependencies 57 | logger $logName "INFO" "Running package updater" 58 | exec_wrapper $ERR_ACTION_EXIT $logName "apt-get update" 59 | 60 | logger $logName "INFO" "Installing dependencies" 61 | exec_wrapper $ERR_ACTION_EXIT $logName "apt-get install -y curl ca-certificates tzdata perl jq coreutils zip p7zip-full" 62 | } 63 | 64 | 65 | set_gitlab_vars () { 66 | local logName=$1 67 | logger $logName "INFO" "Fetching Gitlab network variables" 68 | ## Network variables 69 | INSTANCE_PROTOCOL=`curl -H "Metadata-Flavor: Google" http://169.254.169.254/computeMetadata/v1/instance/attributes/instance-protocol` #http/https 70 | INSTANCE_EXTERNAL_DOMAIN=`curl -H "Metadata-Flavor: Google" http://169.254.169.254/computeMetadata/v1/instance/attributes/instance-external-domain` 71 | EXTERNAL_IP=`curl -H "Metadata-Flavor: Google" http://metadata/computeMetadata/v1/instance/network-interfaces/0/access-configs/0/external-ip` 72 | CONTAINER_REGISTRY_HOST=`curl -H "Metadata-Flavor: Google" http://169.254.169.254/computeMetadata/v1/instance/attributes/container-registry-host` 73 | CONTAINER_REGISTRY_NAMESPACE=`curl -H "Metadata-Flavor: Google" http://169.254.169.254/computeMetadata/v1/instance/attributes/container-registry-namespace` 74 | 75 | #Ext URL 76 | EXTERNAL_URL="$INSTANCE_PROTOCOL://$INSTANCE_EXTERNAL_DOMAIN" 77 | logger $logName "DEBUG" "Gitlab External URL will be $EXTERNAL_URL" 78 | 79 | #Container registry prefix 80 | logger $logName "DEBUG" "Container registry prefix: $CONTAINER_REGISTRY_HOST/$CONTAINER_REGISTRY_NAMESPACE" 81 | 82 | } 83 | 84 | 85 | reconfigure_gitlab () { 86 | # Sets Gitlab's certificate and External URL from configured metadata and secrets 87 | local logName=$1 88 | if [[ $INSTANCE_PROTOCOL == "https" ]] 89 | then 90 | logger $logName "INFO" "Gitlab set to HTTPS, setting self-signed certificate on server" 91 | mkdir -p /etc/gitlab/ssl 92 | chmod 755 /etc/gitlab/ssl 93 | GITLAB_CERT_KEY_SECRET=`curl -H "Metadata-Flavor: Google" http://169.254.169.254/computeMetadata/v1/instance/attributes/gitlab-cert-key-secret` 94 | gcloud secrets versions access latest --secret=$GITLAB_CERT_KEY_SECRET > /etc/gitlab/ssl/$INSTANCE_EXTERNAL_DOMAIN.key 95 | GITLAB_CERT_PUB_SECRET=`curl -H "Metadata-Flavor: Google" http://169.254.169.254/computeMetadata/v1/instance/attributes/gitlab-cert-public-secret` 96 | gcloud secrets versions access latest --secret=$GITLAB_CERT_PUB_SECRET > /etc/gitlab/ssl/$INSTANCE_EXTERNAL_DOMAIN.crt 97 | fi 98 | 99 | # Use the provided external url 100 | 101 | logger $logName "INFO" "Setting Gitlab external url to $EXTERNAL_URL" 102 | echo "external_url \"$EXTERNAL_URL\"" >> /etc/gitlab/gitlab.rb 103 | 104 | # Reconfigure installation 105 | logger $logName "INFO" "Reconfiguring Gitlab" 106 | 107 | exec_wrapper $ERR_ACTION_CONT $logName "gitlab-ctl reconfigure" 108 | 109 | exec_wrapper $ERR_ACTION_CONT $logName "gitlab-ctl restart" 110 | 111 | logger $logName "INFO" "Updating EXTERNAL URL to CI_EXTERNAL_URL CI/CD variable" 112 | echo "Ci::InstanceVariable.where(key: 'CI_EXTERNAL_URL').update(value: '$EXTERNAL_URL')" > $RAILS_CMD_PATH 113 | exec_wrapper $ERR_ACTION_CONT $logName "gitlab-rails runner $RAILS_CMD_PATH" 114 | } 115 | 116 | 117 | gitlab_install () { 118 | local logName=$1 119 | local gitlabPkg=$2 120 | local pkgOutputName="gitlab-ee_amd64.deb" 121 | # Install Postfix non-interactive 122 | logger $logName "INFO" "Starting postfix installation for domain: $INSTANCE_EXTERNAL_DOMAIN" 123 | debconf-set-selections <<< "postfix postfix/mailname string $INSTANCE_EXTERNAL_DOMAIN" 124 | debconf-set-selections <<< "postfix postfix/main_mailer_type string 'Internet Site'" 125 | exec_wrapper $ERR_ACTION_CONT $logName "apt-get install --assume-yes postfix" 126 | 127 | # Install Gitlab Server 128 | logger $logName "INFO" "Setting up Gitlab installation" 129 | curl https://packages.gitlab.com/install/repositories/gitlab/gitlab-ee/script.deb.sh | bash 130 | #Specific version installation: 131 | logger $logName "INFO" "Downloading Gitlab from $gitlabPkg" 132 | exec_wrapper $ERR_ACTION_EXIT $logName "wget --content-disposition -O $pkgOutputName $gitlabPkg" 133 | logger $logName "INFO" "Installing Gitlab" 134 | exec_wrapper $ERR_ACTION_EXIT $logName "dpkg -i $pkgOutputName" 135 | 136 | logger $logName "INFO" "Reconfiguring Gitlab" 137 | exec_wrapper $ERR_ACTION_CONT $logName "gitlab-ctl reconfigure" 138 | } 139 | 140 | 141 | setup_cicd_vars () { 142 | local logName=$1 143 | # Set instance level environment variables, so pipelines can utilize them 144 | logger $logName "INFO" "Fetching values for instance level CI/CD variables" 145 | GCP_PROJECT_ID=`gcloud config list --format 'value(core.project)' 2>/dev/null` 146 | INSTANCE_INTERNAL_HOSTNAME=`curl -H "Metadata-Flavor: Google" "http://metadata.google.internal/computeMetadata/v1/instance/hostname"` 147 | INSTANCE_INTERNAL_URL=$INSTANCE_PROTOCOL://$INSTANCE_INTERNAL_HOSTNAME 148 | INSTANCE_INTERNAL_API_V4_URL=$INSTANCE_PROTOCOL://$INSTANCE_INTERNAL_HOSTNAME/api/v4 149 | 150 | logger $logName "DEBUG" " 151 | CI_EXTERNAL_URL=$EXTERNAL_URL 152 | CI_SERVER_HOST=$INSTANCE_INTERNAL_HOSTNAME 153 | CI_SERVER_URL=$INSTANCE_INTERNAL_URL 154 | CI_API_V4_URL=$INSTANCE_INTERNAL_API_V4_URL 155 | CONTAINER_REGISTRY_NAMESPACE=$CONTAINER_REGISTRY_NAMESPACE 156 | CONTAINER_REGISTRY_HOST=$CONTAINER_REGISTRY_HOST 157 | " 158 | 159 | } 160 | 161 | create_cicd_vars () { 162 | local logName=$1 163 | # New instance level variables 164 | logger $logName "INFO" "Creating instance level CI/CD variables" 165 | 166 | echo "Ci::InstanceVariable.new(key: 'CI_EXTERNAL_URL', value: '$EXTERNAL_URL').save" > $RAILS_CMD_PATH 167 | echo "Ci::InstanceVariable.new(key: 'CI_SERVER_HOST', value: '$INSTANCE_INTERNAL_HOSTNAME').save" >> $RAILS_CMD_PATH 168 | echo "Ci::InstanceVariable.new(key: 'CI_SERVER_URL', value: '$INSTANCE_INTERNAL_URL').save" >> $RAILS_CMD_PATH 169 | echo "Ci::InstanceVariable.new(key: 'CI_API_V4_URL', value: '$INSTANCE_INTERNAL_API_V4_URL').save" >> $RAILS_CMD_PATH 170 | echo "Ci::InstanceVariable.new(key: 'CONTAINER_REGISTRY_NAMESPACE', value: '$CONTAINER_REGISTRY_NAMESPACE').save" >> $RAILS_CMD_PATH 171 | echo "Ci::InstanceVariable.new(key: 'CONTAINER_REGISTRY_HOST', value: '$CONTAINER_REGISTRY_HOST').save" >> $RAILS_CMD_PATH 172 | 173 | exec_wrapper $ERR_ACTION_CONT $logName "gitlab-rails runner $RAILS_CMD_PATH" 174 | 175 | } 176 | 177 | update_cicd_vars () { 178 | local logName=$1 179 | # Update instance level variables values accroding to the new GCP project and compute hostname 180 | logger $logName "INFO" "Updating instance level CI/CD variables" 181 | 182 | echo "Ci::InstanceVariable.where(key: 'CI_SERVER_HOST').update(value: '$INSTANCE_INTERNAL_HOSTNAME')" > $RAILS_CMD_PATH 183 | echo "Ci::InstanceVariable.where(key: 'CI_SERVER_URL').update(value: '$INSTANCE_INTERNAL_URL')" >> $RAILS_CMD_PATH 184 | echo "Ci::InstanceVariable.where(key: 'CI_API_V4_URL').update(value: '$INSTANCE_INTERNAL_API_V4_URL')" >> $RAILS_CMD_PATH 185 | echo "Ci::InstanceVariable.where(key: 'CONTAINER_REGISTRY_NAMESPACE').update(value: '$CONTAINER_REGISTRY_NAMESPACE').save" >> $RAILS_CMD_PATH 186 | echo "Ci::InstanceVariable.where(key: 'CONTAINER_REGISTRY_HOST').update(value: '$CONTAINER_REGISTRY_HOST').save" >> $RAILS_CMD_PATH 187 | 188 | exec_wrapper $ERR_ACTION_CONT $logName "gitlab-rails runner $RAILS_CMD_PATH" 189 | } 190 | 191 | 192 | seed_instance_reg_token () { 193 | local logName=$1 194 | # Seed shared runners registartion token 195 | GITLAB_RUNNER_REG_SECRET=`curl -H "Metadata-Flavor: Google" http://169.254.169.254/computeMetadata/v1/instance/attributes/gitlab-ci-runner-registration-token-secret` 196 | logger $logName "INFO" "Reading shared runners registration token from secret $GITLAB_RUNNER_REG_SECRET" 197 | GITLAB_RUNNER_REG=`gcloud secrets versions access latest --secret=$GITLAB_RUNNER_REG_SECRET` 198 | logger $logName "INFO" "Seeding shared runners registration token" 199 | 200 | echo "appset = Gitlab::CurrentSettings.current_application_settings; appset.set_runners_registration_token('$GITLAB_RUNNER_REG'); appset.save!" > $RAILS_CMD_PATH 201 | exec_wrapper $ERR_ACTION_CONT $logName "gitlab-rails runner $RAILS_CMD_PATH" 202 | } 203 | 204 | 205 | seed_gitlab_root_pwd () { 206 | local logName=$1 207 | # Seed gitlab root password 208 | GITLAB_INITIAL_ROOT_PASSWORD_SECRET=`curl -H "Metadata-Flavor: Google" http://169.254.169.254/computeMetadata/v1/instance/attributes/gitlab-initial-root-pwd-secret` 209 | logger $logName "INFO" "Reading gitlab root password from secret $GITLAB_INITIAL_ROOT_PASSWORD_SECRET" 210 | GITLAB_INITIAL_ROOT_PASSWORD=`gcloud secrets versions access latest --secret=$GITLAB_INITIAL_ROOT_PASSWORD_SECRET` 211 | 212 | logger $logName "INFO" "Seeding gitlab root password" 213 | echo "user = User.find_by_username('root'); user.password = '$GITLAB_INITIAL_ROOT_PASSWORD'; user.password_confirmation = '$GITLAB_INITIAL_ROOT_PASSWORD'; user.save!" > $RAILS_CMD_PATH 214 | exec_wrapper $ERR_ACTION_CONT $logName "gitlab-rails runner $RAILS_CMD_PATH" 215 | } 216 | 217 | 218 | 219 | create_groups () { 220 | local logName=$1 221 | # Create repo groups (ci, community, private) 222 | logger $logName "INFO" "Creating groups: ci, community, private" 223 | 224 | echo "Groups::CreateService.new(User.find_by_id(1), params = {name: 'CI CD Tools', path: 'ci', visibility_level: 10}).execute" > $RAILS_CMD_PATH 225 | echo "Groups::CreateService.new(User.find_by_id(1), params = {name: 'Community Tools', path: 'community', visibility_level: 10}).execute" >> $RAILS_CMD_PATH 226 | echo "Groups::CreateService.new(User.find_by_id(1), params = {name: 'Private Tools', path: 'private', visibility_level: 10}).execute" >> $RAILS_CMD_PATH 227 | 228 | exec_wrapper $ERR_ACTION_CONT $logName "gitlab-rails runner $RAILS_CMD_PATH" 229 | } 230 | 231 | 232 | import_scallops_recipes () { 233 | local logName=$1 234 | # Import SCALLOPS-RECIPES project repo 235 | readonly SCALLOPS_RECIPES_GIT_URL=`curl -H "Metadata-Flavor: Google" http://169.254.169.254/computeMetadata/v1/instance/attributes/scallops-recipes-git-url` 236 | local gitCredsSecretName=`curl -H "Metadata-Flavor: Google" http://169.254.169.254/computeMetadata/v1/instance/attributes/scallops-recipes-git-creds-secret` 237 | logger $logName "INFO" "Importing SCALLOPS-RECIPES repo from $SCALLOPS_RECIPES_GIT_URL to ci group" 238 | 239 | if [ $gitCredsSecretName != 'NONE' ]; then 240 | logger $logName "INFO" "Reading Git credentials from secret $gitCredsSecretName to import $SCALLOPS_RECIPES_GIT_URL repository" 241 | local gitCreds=`gcloud secrets versions access latest --secret=$gitCredsSecretName` 242 | SCALLOPS_RECIPES_GIT_URL="https://$gitCreds@${SCALLOPS_RECIPES_GIT_URL:8}" 243 | fi 244 | 245 | 246 | logger $logName "DEBUG" "Ignore the following error: (undefined method repository for :octokit:Symbol)" 247 | echo "cigrp = Group.find_by_path_or_name('ci'); rootuser = User.find_by_id(1); Project.new(import_url: '$SCALLOPS_RECIPES_GIT_URL', name: 'Scallops Recipes', path: 'scallops-recipes', visibility_level: 10, creator: rootuser, namespace: cigrp).save" > $RAILS_CMD_PATH 248 | echo "newprj = Project.find_by_full_path('ci/scallops-recipes'); Gitlab::GithubImport::Importer::RepositoryImporter.new(newprj, :octokit).execute" >> $RAILS_CMD_PATH 249 | 250 | exec_wrapper $ERR_ACTION_CONT $logName "gitlab-rails runner $RAILS_CMD_PATH" # Ignore the following error (undefined method `repository' for :octokit:Symbol) 251 | } 252 | 253 | 254 | seed_scallops_recipes_runner_token () { 255 | local logName=$1 256 | # Seed scallops-recipes sepcific runners registration token 257 | # Make sure to invoke seed_instance_reg_token function before this one, so the $GITLAB_RUNNER_REG variable will be set 258 | logger $logName "INFO" "Seeding Scallops-Recipes runners registration token" 259 | local scallopsRunnerReg=GR1348941$GITLAB_RUNNER_REG-scallops-recipes 260 | 261 | echo "scallopsprj = Project.find_by_full_path('ci/scallops-recipes'); scallopsprj.set_runners_token('$scallopsRunnerReg'); scallopsprj.save!" > $RAILS_CMD_PATH 262 | exec_wrapper $ERR_ACTION_CONT $logName "gitlab-rails runner $RAILS_CMD_PATH" 263 | } 264 | 265 | 266 | setup_gitlab_backup () { 267 | local logName=$1 268 | local gcsPrefix=$2 269 | # Download backup cron executor and cron job #Backup will occur every Saturday on 10:00 UTC 270 | logger $logName "INFO" "Setting up backup procedure with crontab" 271 | exec_wrapper $ERR_ACTION_CONT $logName "gsutil cp $gcsPrefix/scripts/bash/gitlab_backup.sh /gitlab_backup.sh" 272 | chmod +x /gitlab_backup.sh 273 | echo "0 10 * * 6 /gitlab_backup.sh" > gitlab-backup-cron 274 | logger $logName "DEBUG" "Crontab content $(cat gitlab-backup-cron)" 275 | exec_wrapper $ERR_ACTION_CONT $logName "crontab gitlab-backup-cron" 276 | rm gitlab-backup-cron 277 | } 278 | 279 | 280 | 281 | get_backup_archive () { 282 | local logName=$1 283 | local gcsPathToBackup=$2 284 | local backupArchivePath=$3 285 | 286 | # Download backup 287 | logger $logName "INFO" "Downloading backup from $gcsPathToBackup" 288 | exec_wrapper $ERR_ACTION_CONT $logName "gsutil cp $gcsPathToBackup $backupArchivePath" 289 | 290 | } 291 | 292 | 293 | get_backup_archive_password () { 294 | local logName=$1 295 | 296 | # Get the backup archive password 297 | logger $logName "INFO" "Getting backup password secret from instance metadata" 298 | GITLAB_BACKUP_PASSWORD_SECRET=`curl -H "Metadata-Flavor: Google" http://169.254.169.254/computeMetadata/v1/instance/attributes/gitlab-backup-key-secret` 299 | logger $logName "INFO" "Reading password for the backup archive from secret - $GITLAB_BACKUP_PASSWORD_SECRET" 300 | GITLAB_BACKUP_PASSWORD=`gcloud secrets versions access latest --secret=$GITLAB_BACKUP_PASSWORD_SECRET` 301 | logger $logName "DEBUG" "Fetched password with length of ${#GITLAB_BACKUP_PASSWORD} characters" 302 | } 303 | 304 | 305 | restore_backup () { 306 | local logName=$1 307 | local backupArchivePath=$2 308 | local backupDir="backup-extracted" 309 | 310 | # Migrate configuration files and SSL certificates 311 | 312 | mkdir -p $backupDir 313 | 314 | logger $logName "INFO" "Extracting backup from $backupArchivePath to $backupDir" 315 | exec_wrapper_no_print $ERR_ACTION_EXIT $logName "7z x -p$GITLAB_BACKUP_PASSWORD $backupArchivePath -o./$backupDir" 316 | 317 | # Stop Gitlab services 318 | logger $logName "INFO" "Stopping Gitlab services: puma, sidekiq" 319 | gitlab-ctl stop puma 320 | gitlab-ctl stop sidekiq 321 | gitlab-ctl status 322 | 323 | logger $logName "INFO" "Copying configuration files and certificates from $backupDir" 324 | cp $backupDir/gitlab* /etc/gitlab 325 | cp -R $backupDir/ssl /etc/gitlab 326 | 327 | # Restore from backup 328 | local backupFileName=$(ls $backupDir | grep _gitlab_backup.tar) 329 | logger $logName "INFO" "Copying extracted backup archive" 330 | cp $backupDir/$backupFileName /var/opt/gitlab/backups/ 331 | chown git:git /var/opt/gitlab/backups/$backupFileName 332 | 333 | logger $logName "INFO" "Restoring from backup snapshot... $backupFileName" 334 | local restoreBackupName=$(echo $backupFileName | cut -d "_" -f 1-5) 335 | exec_wrapper_no_print $ERR_ACTION_EXIT $logName "gitlab-rake gitlab:backup:restore BACKUP=$restoreBackupName force=yes" 336 | 337 | logger $logName "INFO" "Reconfiguring Gitlab" 338 | exec_wrapper $ERR_ACTION_CONT $logName "gitlab-ctl reconfigure" 339 | 340 | logger $logName "INFO" "Restarting Gitlab services" 341 | exec_wrapper $ERR_ACTION_CONT $logName "gitlab-ctl restart" 342 | 343 | logger $logName "INFO" "Checking Gitlab services health" 344 | exec_wrapper $ERR_ACTION_CONT $logName "gitlab-rake gitlab:check SANITIZE=true" 345 | 346 | logger $logName "INFO" "Checking secrets decryptability" 347 | exec_wrapper $ERR_ACTION_CONT $logName "gitlab-rake gitlab:doctor:secrets" 348 | } 349 | 350 | 351 | 352 | execute_backup () { 353 | ### Scallops customized Gitlab backup script ### 354 | ## Run as root 355 | 356 | local logName=$1 357 | local gitlabVersion=$2 358 | local instanceName=`curl -H "Metadata-Flavor: Google" http://169.254.169.254/computeMetadata/v1/instance/name` 359 | local timestamp=`date +"%s"` 360 | local backupDir="backup-$timestamp" 361 | local backupArchiveFile="$instanceName-$gitlabVersion-$backupDir.zip" 362 | 363 | 364 | logger $logName "INFO" "Starting Gitlab backup for $instanceName" 365 | 366 | # Create backup folder 367 | mkdir -p $backupDir 368 | 369 | # Stop Gitlab services 370 | logger $logName "INFO" "Stopping Gitlab services (unicorn, sidekiq, puma)" 371 | gitlab-ctl stop unicorn 372 | gitlab-ctl stop sidekiq 373 | gitlab-ctl stop puma 374 | 375 | # Create back up TAR 376 | logger $logName "INFO" "Creaing backup tar file" 377 | exec_wrapper $ERR_ACTION_EXIT $logName "gitlab-backup create" 378 | 379 | # Restart Gitlab services back 380 | logger $logName "INFO" "Restarting gitlab services" 381 | exec_wrapper $ERR_ACTION_CONT $logName "gitlab-ctl restart" 382 | 383 | # Copy DB backup and configurations 384 | local mostRecentBackupName=`ls -t /var/opt/gitlab/backups/ | head -1` 385 | logger $logName "INFO" "Using backup: $mostRecentBackupName" 386 | 387 | logger $logName "INFO" "Copying DB backup, gitlab configurations and SSL ceritficates" 388 | mv /var/opt/gitlab/backups/$mostRecentBackupName $backupDir/ 389 | cp /etc/gitlab/gitlab.rb $backupDir/ 390 | cp /etc/gitlab/gitlab-secrets.json $backupDir/ 391 | cp -R /etc/gitlab/ssl/ $backupDir/ 392 | 393 | # Get the backup bucket name 394 | logger $logName "INFO" "Getting backup bucket name from instance metadata" 395 | GITLAB_BACKUPS_BUCKET_NAME=`curl -H "Metadata-Flavor: Google" http://169.254.169.254/computeMetadata/v1/instance/attributes/gitlab-backup-bucket-name` 396 | 397 | 398 | # Archive and encrypt backup 399 | logger $logName "INFO" "Archiving and encrypting backup" 400 | exec_wrapper_no_print $ERR_ACTION_EXIT $logName "7z a -p$GITLAB_BACKUP_PASSWORD $backupDir.zip ./$backupDir/*" 401 | 402 | # Upload archived backup 403 | logger $logName "INFO" "Uploading backup as: gs://$GITLAB_BACKUPS_BUCKET_NAME/gitlab-backups/$backupArchiveFile" 404 | exec_wrapper $ERR_ACTION_CONT $logName "gsutil cp $backupDir.zip gs://$GITLAB_BACKUPS_BUCKET_NAME/gitlab-backups/$backupArchiveFile" 405 | 406 | 407 | # Delete source directory and backup archive 408 | logger $logName "INFO" "Deleting processed files" 409 | rm -r $backupDir 410 | rm $backupDir.zip 411 | 412 | logger $logName "INFO" "Gitlab backup completed" 413 | } -------------------------------------------------------------------------------- /scripts/bash/gitlab_startup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Gitlab starup script 4 | # This script will either install, migrate or reconfigure Gitlab installation according to the values set in the instance metadata. 5 | # Installation / Migration will be executed once in a life of an instance. 6 | # Reconfiguration will be executed on every boot. 7 | # Reconfiguration updates the Gitlab's External URL and certificates if they changed through metadata. 8 | 9 | 10 | #Vars 11 | DEPLOYMENT_GCS_PREFIX=`curl -H "Metadata-Flavor: Google" http://169.254.169.254/computeMetadata/v1/instance/attributes/gcs-prefix` 12 | GITLAB_TARGET_INSTALL_VERSION=`curl -H "Metadata-Flavor: Google" http://169.254.169.254/computeMetadata/v1/instance/attributes/target-gitlab-version` 13 | GITLAB_PKG_LINK=`curl -H "Metadata-Flavor: Google" http://169.254.169.254/computeMetadata/v1/instance/attributes/gitlab-package-dl-link` 14 | GCS_PATH_TO_BACKUP=`curl -H "Metadata-Flavor: Google" http://169.254.169.254/computeMetadata/v1/instance/attributes/gcs-path-to-backup` 15 | GCLOUD_LOG_NAME="gitlab-startup" 16 | BACKUP_ARCHIVE_PATH="/tmp/backup_archived.zip" 17 | 18 | #Imports 19 | gsutil cp $DEPLOYMENT_GCS_PREFIX/scripts/bash/gitlab_helpers.sh ./ 20 | gsutil cp $DEPLOYMENT_GCS_PREFIX/scripts/bash/gcloud_logger.sh ./ 21 | source ./gcloud_logger.sh 22 | source ./gitlab_helpers.sh 23 | 24 | 25 | # Start 26 | logger $GCLOUD_LOG_NAME "INFO" "Starting Gitlab instance setup" 27 | 28 | check_installation $GCLOUD_LOG_NAME 29 | set_gitlab_vars $GCLOUD_LOG_NAME 30 | 31 | if [ $GITLAB_INSTALLED == 'false' ]; then 32 | logger $GCLOUD_LOG_NAME "INFO" "Starting Gitlab installation" 33 | gitlab_deps_install $GCLOUD_LOG_NAME 34 | gitlab_install $GCLOUD_LOG_NAME $GITLAB_PKG_LINK 35 | setup_cicd_vars $GCLOUD_LOG_NAME 36 | 37 | if [ $GCS_PATH_TO_BACKUP == 'NONE' ]; then 38 | create_groups $GCLOUD_LOG_NAME 39 | import_scallops_recipes $GCLOUD_LOG_NAME 40 | create_cicd_vars $GCLOUD_LOG_NAME 41 | 42 | else 43 | logger $GCLOUD_LOG_NAME "INFO" "Migrating Gitlab from provided backup $GCS_PATH_TO_BACKUP" 44 | get_backup_archive $GCLOUD_LOG_NAME $GCS_PATH_TO_BACKUP $BACKUP_ARCHIVE_PATH 45 | get_backup_archive_password $GCLOUD_LOG_NAME 46 | restore_backup $GCLOUD_LOG_NAME $BACKUP_ARCHIVE_PATH 47 | update_cicd_vars $GCLOUD_LOG_NAME 48 | fi 49 | 50 | seed_instance_reg_token $GCLOUD_LOG_NAME 51 | seed_gitlab_root_pwd $GCLOUD_LOG_NAME 52 | seed_scallops_recipes_runner_token $GCLOUD_LOG_NAME 53 | setup_gitlab_backup $GCLOUD_LOG_NAME $DEPLOYMENT_GCS_PREFIX 54 | 55 | else 56 | logger $GCLOUD_LOG_NAME "INFO" "Skipping Gitlab installation" 57 | check_upgrade $GCLOUD_LOG_NAME $GITLAB_TARGET_INSTALL_VERSION 58 | fi 59 | 60 | 61 | reconfigure_gitlab $GCLOUD_LOG_NAME 62 | logger $GCLOUD_LOG_NAME "INFO" "Gitlab instance setup completed" -------------------------------------------------------------------------------- /secrets.tf: -------------------------------------------------------------------------------- 1 | #### Secret creation and version manager #### 2 | 3 | 4 | # Self signed TLS certificate generation 5 | 6 | resource "tls_private_key" "gitlab-self-signed-cert-key" { 7 | algorithm = "ECDSA" 8 | ecdsa_curve = "P384" 9 | } 10 | 11 | resource "tls_self_signed_cert" "gitlab-self-signed-cert" { 12 | private_key_pem = tls_private_key.gitlab-self-signed-cert-key.private_key_pem 13 | 14 | subject { 15 | common_name = "gitlab.local" 16 | organization = "Company" 17 | } 18 | 19 | dns_names = flatten([ 20 | "${local.gitlab_instance_name}.local", 21 | local.instance_internal_domain, 22 | var.external_hostname != "" ? [var.external_hostname] : [] 23 | ]) 24 | 25 | ip_addresses = ["10.0.0.2"] 26 | validity_period_hours = 87600 //Certificate will be valid for 10 years 27 | 28 | allowed_uses = [ 29 | "key_encipherment", 30 | "digital_signature" 31 | ] 32 | } 33 | 34 | 35 | # Gitlab server certificate 36 | 37 | resource "google_secret_manager_secret" "gitlab-self-signed-cert-key" { 38 | provider = google.offensive-pipeline 39 | secret_id = "${local.gitlab_instance_name}-cert-key" 40 | labels = { 41 | label = "gitlab-cert" 42 | } 43 | replication { 44 | user_managed { 45 | replicas { 46 | location = var.region 47 | } 48 | } 49 | } 50 | } 51 | 52 | 53 | resource "google_secret_manager_secret_version" "gitlab-self-signed-cert-key-version" { 54 | secret = google_secret_manager_secret.gitlab-self-signed-cert-key.id 55 | secret_data = tls_private_key.gitlab-self-signed-cert-key.private_key_pem 56 | } 57 | 58 | 59 | resource "google_secret_manager_secret" "gitlab-self-signed-cert-crt" { 60 | provider = google.offensive-pipeline 61 | secret_id = "${local.gitlab_instance_name}-cert-crt" 62 | labels = { 63 | label = "gitlab-cert" 64 | } 65 | replication { 66 | user_managed { 67 | replicas { 68 | location = var.region 69 | } 70 | } 71 | } 72 | } 73 | 74 | 75 | resource "google_secret_manager_secret_version" "gitlab-self-signed-cert-crt-version" { 76 | secret = google_secret_manager_secret.gitlab-self-signed-cert-crt.id 77 | secret_data = tls_self_signed_cert.gitlab-self-signed-cert.cert_pem 78 | } 79 | 80 | 81 | 82 | # Gitlab installation & deployment 83 | 84 | resource "random_password" "gitlab_runner_registration_token" { 85 | length = 20 86 | special = true 87 | override_special = "-_" 88 | } 89 | resource "random_password" "gitlab_initial_root_pwd" { 90 | length = 16 91 | special = true 92 | override_special = "-_" 93 | } 94 | 95 | 96 | # Gitlab runner registration token 97 | 98 | resource "google_secret_manager_secret" "gitlab_runner_registration_token" { 99 | provider = google.offensive-pipeline 100 | secret_id = "${local.gitlab_instance_name}-runner-reg" 101 | labels = { 102 | label = "gitlab" 103 | } 104 | replication { 105 | user_managed { 106 | replicas { 107 | location = var.region 108 | } 109 | } 110 | } 111 | } 112 | 113 | 114 | resource "google_secret_manager_secret_version" "gitlab_runner_registration_token" { 115 | secret = google_secret_manager_secret.gitlab_runner_registration_token.id 116 | secret_data = random_password.gitlab_runner_registration_token.result 117 | } 118 | 119 | 120 | # Gitlab initial root password 121 | resource "google_secret_manager_secret" "gitlab_initial_root_pwd" { 122 | provider = google.offensive-pipeline 123 | secret_id = "${local.gitlab_instance_name}-root-password" 124 | labels = { 125 | label = "gitlab" 126 | } 127 | replication { 128 | user_managed { 129 | replicas { 130 | location = var.region 131 | } 132 | } 133 | } 134 | } 135 | 136 | 137 | resource "google_secret_manager_secret_version" "gitlab_initial_root_pwd" { 138 | secret = google_secret_manager_secret.gitlab_initial_root_pwd.id 139 | secret_data = random_password.gitlab_initial_root_pwd.result 140 | } 141 | 142 | 143 | # Docker hub credentials secret 144 | 145 | data "google_secret_manager_secret_version" "dockerhub-secret" { 146 | provider = google.offensive-pipeline 147 | count = var.dockerhub-creds-secret != "" ? 1 : 0 148 | secret = var.dockerhub-creds-secret 149 | } -------------------------------------------------------------------------------- /storage.tf: -------------------------------------------------------------------------------- 1 | # CICD utilities Storage 2 | 3 | resource "random_string" "deployment_utils_bucket_suffix" { 4 | length = 4 5 | special = false 6 | lower = true 7 | upper = false 8 | } 9 | 10 | resource "google_storage_bucket" "deployment_utils" { 11 | name = "${var.infra_name}-utils-${random_string.deployment_utils_bucket_suffix.result}" 12 | location = "US" 13 | storage_class = "STANDARD" 14 | uniform_bucket_level_access = true 15 | provider = google.offensive-pipeline 16 | force_destroy = true 17 | } 18 | 19 | 20 | resource "google_storage_bucket_object" "disable_windows_defender_ps" { 21 | name = "scripts/Powershell/disabledefender.ps1" 22 | bucket = google_storage_bucket.deployment_utils.name 23 | source = "${path.module}/scripts/Powershell/disabledefender.ps1" 24 | } 25 | 26 | 27 | resource "google_storage_bucket_object" "gitlab_startup_script" { 28 | depends_on = [ 29 | google_storage_bucket_object.gitlab_helpers_script, 30 | google_storage_bucket_object.gcloud_logger_script, 31 | google_storage_bucket_object.gitlab_backup_script] 32 | 33 | name = "scripts/bash/gitlab_startup.sh" 34 | bucket = google_storage_bucket.deployment_utils.name 35 | source = "${path.module}/scripts/bash/gitlab_startup.sh" 36 | } 37 | 38 | resource "google_storage_bucket_object" "gitlab_helpers_script" { 39 | name = "scripts/bash/gitlab_helpers.sh" 40 | bucket = google_storage_bucket.deployment_utils.name 41 | source = "${path.module}/scripts/bash/gitlab_helpers.sh" 42 | } 43 | 44 | resource "google_storage_bucket_object" "gcloud_logger_script" { 45 | name = "scripts/bash/gcloud_logger.sh" 46 | bucket = google_storage_bucket.deployment_utils.name 47 | source = "${path.module}/scripts/bash/gcloud_logger.sh" 48 | } 49 | 50 | resource "google_storage_bucket_object" "gitlab_backup_script" { 51 | depends_on = [google_storage_bucket_object.gitlab_helpers_script, 52 | google_storage_bucket_object.gcloud_logger_script] 53 | 54 | name = "scripts/bash/gitlab_backup.sh" 55 | bucket = google_storage_bucket.deployment_utils.name 56 | source = "${path.module}/scripts/bash/gitlab_backup.sh" 57 | } 58 | 59 | # Migration resource 60 | 61 | # Transfer is done in this way since state file won't support big files in resource attributes. 62 | resource "null_resource" "transfer_gitlab_backup" { 63 | count = var.migrate_gitlab ? 1 : 0 64 | triggers = { 65 | gcs_backup_path = "gs://${var.migrate_gitlab_backup_bucket}/${var.migrate_gitlab_backup_path}" 66 | } 67 | provisioner "local-exec" { 68 | when = create 69 | command = "gsutil cp gs://${var.migrate_gitlab_backup_bucket}/${var.migrate_gitlab_backup_path} ${local.gitlab_migrate_backup}" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /vars.tf: -------------------------------------------------------------------------------- 1 | # Deployment wide variables 2 | variable "project_id" { 3 | type = string 4 | description = "(required) GCP Project ID to deploy to" 5 | } 6 | 7 | variable "dns_project_id" { 8 | type = string 9 | description = "If provided external_hostname, specify GCP Project ID where managed zone is located" 10 | default = "" 11 | } 12 | 13 | variable "infra_name" { 14 | type = string 15 | description = "(required) Infrastructure name or Team name" 16 | validation { 17 | condition = can(regex("^[a-z]([a-z0-9]*[a-z0-9])$", var.infra_name)) // Due to Certificate SAN, and service account name translation to email, and 1 another. 18 | error_message = "The infrastructure name must comply with the following regex ^[a-z]([a-z0-9]*[a-z0-9])$ )." 19 | } 20 | } 21 | 22 | # Backup variables 23 | variable "backups_bucket_name" { 24 | type = string 25 | description = "The name of the bucket backups are stored. Bucket must exist before apply. Terrafrom will add objectCreator permission to the gitlab svc account." 26 | } 27 | 28 | variable "gitlab_backup_key_secret_id" { 29 | description = "An existing secret ID in the same GCP project (project_id) storing a password for the backup process (Allowed symbols: -_ )" 30 | type = string 31 | default = "" 32 | } 33 | 34 | # Migration variables 35 | variable "migrate_gitlab" { 36 | type = bool 37 | description = "If performing migration from another Gitlab instance and got a backup file from previous instance" 38 | default = false 39 | } 40 | 41 | 42 | variable "migrate_gitlab_backup_bucket" { 43 | type = string 44 | description = "The Google Storage Bucket to your Gitlab backup e.g. 'mybucket1-abcd'" 45 | default = "" 46 | } 47 | 48 | variable "migrate_gitlab_backup_path" { 49 | type = string 50 | description = "The path to the archived backup zip 'backups/gitlab-xxx-backup.zip'" 51 | default = "" 52 | } 53 | 54 | 55 | 56 | # Gitlab instance related variables 57 | 58 | variable "gitlab_instance_protocol" { 59 | type = string 60 | description = "(optional) Protocol to use for Gitlab instance http / https" 61 | default = "https" 62 | validation { 63 | condition = can(regex("^https?$", var.gitlab_instance_protocol)) 64 | error_message = "The gitlab_instance_protocol can be either http/https." 65 | } 66 | } 67 | 68 | variable "gitlab_version" { 69 | type = string 70 | description = "Gitlab version to install (e.g. 15.2.1-ee). If performing migration, you must specify the Gitlab backup version from the previous instance" 71 | default = "16.1.2-ee" 72 | validation { 73 | condition = can(regex("^[0-9]+.[0-9]+.[0-9]+-ee$", var.gitlab_version)) 74 | error_message = "Invalid Gitlab version" 75 | } 76 | } 77 | 78 | variable "plans" { 79 | type = map 80 | default = { 81 | "2x8" = "n1-standard-2" #(56.72$, 0.097118$) 82 | } 83 | } 84 | 85 | variable "size" { 86 | type = string 87 | default = "2x8" 88 | } 89 | 90 | variable "os_images" { 91 | type = map 92 | default = { 93 | "ubuntu" = { 94 | "jammy" = "ubuntu-2204-lts" 95 | "focal" = "ubuntu-2004-lts" 96 | "bionic" = "ubuntu-1804-lts" 97 | } 98 | } 99 | } 100 | 101 | variable "os_name" { 102 | type = string 103 | description = "(optional) OS Image" 104 | default = "ubuntu" 105 | } 106 | 107 | variable "os_release" { 108 | type = string 109 | description = "(optional) OS Image" 110 | default = "focal" 111 | } 112 | 113 | variable "scallops_recipes_git_url" { 114 | type = string 115 | description = "Scallops-Recipes repository. Git URL must be provided in the following format: https:////.git" 116 | default = "https://github.com/SygniaLabs/ScallOps-Recipes.git" 117 | } 118 | 119 | variable "scallops_recipes_git_creds_secret" { 120 | type = string 121 | description = "A secret in the same project (project_id) storing Git credentials to access the provided scallops-recipes repository. Format is : or ." 122 | default = "" 123 | } 124 | 125 | # Networking and region related variables 126 | 127 | variable "operator_ips" { 128 | type = list(string) 129 | description = "(required) IP addresses used to operate and access Gitlab" 130 | } 131 | 132 | variable "operator_ports" { 133 | type = list(string) 134 | description = "(optional) Ports used to operate Gitlab" 135 | default = ["443","80"] 136 | } 137 | 138 | variable "external_integration_ranges" { 139 | type = list(string) 140 | description = "(optional) IP addresses used to for external integration with Gitlab" 141 | default = null 142 | } 143 | 144 | variable "region" { 145 | type = string 146 | description = "(optional) Region in which Gitlab and K8s will be deployed" 147 | default = "us-central1" 148 | } 149 | 150 | variable "zone" { 151 | type = string 152 | description = "(optional) Zone in which Gitlab GCE and K8s cluster will be deployed, K8s cluster will be Zonal and not Regional." 153 | default = "a" 154 | } 155 | 156 | 157 | 158 | # GKE related variables 159 | variable "gke_version" { 160 | description = "Kubernetes engine version" 161 | type = string 162 | default = "1.26.5-gke.1200" # Available version -> https://cloud.google.com/kubernetes-engine/docs/release-notes 163 | } 164 | 165 | variable "gke_linux_pool_version" { 166 | description = "GKE Linux node pool version" 167 | type = string 168 | default = "1.26.5-gke.1200" 169 | } 170 | 171 | variable "gke_windows_pool_version" { 172 | description = "GKE Windows node pool version" 173 | type = string 174 | default = "1.26.5-gke.1200" 175 | } 176 | 177 | variable "runner_chart_url" { 178 | description = "Gitlab runner Helm chart archive URL" # https://artifacthub.io/packages/helm/gitlab/gitlab-runner 179 | type = string 180 | default = "https://gitlab-charts.s3.amazonaws.com/gitlab-runner-0.54.0.tgz" # Correspond to Gitlab 16.1.0 181 | } 182 | 183 | 184 | # DNS and managed zone variables 185 | variable "external_hostname" { 186 | description = "The external hostname to be configured for the instance. e.g. scallops.example.com" 187 | type = string 188 | default = "" 189 | } 190 | 191 | variable "dns_managed_zone_name" { 192 | description = "The name of the Cloud DNS Managed Zone in which to create the DNS A Records specified in external_hostname. Only use if provided external_hostname. e.g. example-com" 193 | type = string 194 | default = "" 195 | } 196 | 197 | variable "dns_record_ttl" { 198 | description = "The time-to-live for the site A records (seconds)" 199 | type = number 200 | default = 300 201 | } 202 | 203 | variable "dockerhub-creds-secret" { 204 | description = "An existing secret name in the same GCP project storing the Dockerhub credentials (username:password)" 205 | type = string 206 | default = "" 207 | } 208 | 209 | variable "debug_flag" { 210 | type = bool 211 | description = "Enable debugging resources such as IAP Firewall rules, and export of config files" 212 | default = false 213 | } 214 | -------------------------------------------------------------------------------- /versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">=1.5.0" 3 | 4 | required_providers { 5 | google = { 6 | source = "hashicorp/google" 7 | version = ">=4.77.0" 8 | } 9 | kubernetes = { 10 | source = "hashicorp/kubernetes" 11 | version = ">= 2.10.0" 12 | } 13 | helm = { 14 | source = "hashicorp/helm" 15 | version = ">= 2.5.1" 16 | } 17 | tls = { 18 | source = "hashicorp/tls" 19 | version = ">= 3.3.0" 20 | } 21 | google-beta = { 22 | source = "hashicorp/google-beta" 23 | version = ">= 4.44.0" 24 | } 25 | } 26 | } --------------------------------------------------------------------------------