Skip to content

Commit

Permalink
feat: add SSH access using OS Login
Browse files Browse the repository at this point in the history
- Add `os_login` resource to securely SSH into VM instance created by
the Terraform IaC template
- Update docs to include information on how to add and use SSH keys

Fixes issues #10 #1
  • Loading branch information
sydrawat01 committed Oct 7, 2023
1 parent b291c02 commit c34d8d8
Show file tree
Hide file tree
Showing 13 changed files with 93 additions and 41 deletions.
39 changes: 38 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,12 @@ Here's a list of required roles:
To `ssh` into the VM instance, we will have to add the public SSH key into the project [`metadata`](https://console.cloud.google.com/compute/metadata).
This can also be [done via Terraform](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_project_metadata.html#example-usage---adding-an-ssh-key).

> A key set in project metadata is propagated to every instance in the project.
This resource configuration is prone to causing frequent diffs as Google adds SSH Keys when the SSH Button is pressed in the console.
It is better to use OS Login instead.

1. Using project metadata resource

```tf
/*
A key set in project metadata is propagated to every instance in the project.
Expand All @@ -190,6 +196,35 @@ resource "google_compute_project_metadata" "my_ssh_key" {
}
```

2. Using instance metadata resource

```tf
resource "google_compute_instance" "vm" {
metadata = {
ssh-keys = "username:${file("~/.ssh/<ssh-key>.pub")}"
}
}
```

> NOTE: We can also send a file in the `ssh-keys` metadata instead of plain-text public ssh key file.
3. OS Login at User level (recommended method)

Follow the [Terraform documentation on OS login](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/os_login_ssh_public_key) on how to setup the `google_os_login_ssh_public_key` resource. Once complete, we need to add the metadata `enable-oslogin` property to `TRUE` to enable user login using SSH.
> To use OS login, we need to create the infrastructure on Terraform using the `application-default user`.
```tf
resource "google_compute_instance" "vm" {
metadata = {
# Enable os-login through metadata
enable-oslogin : "TRUE"
}
}
```

> NOTE: Since we do not set a default username with OS login, we need to use `USERNAME_DOMAIN_SUFFIX` to ssh into the VM.
[Reference](https://cloud.google.com/compute/docs/oslogin/set-up-oslogin#connect_to_vms_that_have_os_login_enabled)

#### Generate the SSH key locally

To generate the ssh key locally on your workstation, use the following command:
Expand All @@ -205,6 +240,8 @@ Use the below command to connect to the instance:

```bash
ssh -i <path-to-private-key> <username>@<external-ip>
# if os login is enabled:
ssh -i <path-to-private-key> <USERNAME_DOMAIN_SUFFIX>@<external-ip>
```

### 🕹️ Enabling APIs
Expand All @@ -218,7 +255,7 @@ Below is a non-exhaustive list of APIs that can come in handy:
- `container.googleapis.com`
- `orgpolicy.googleapis.com`

> NOTE: Remember to add timed delays to resources when creating them via Terraform.
> NOTE: Remember to add timed delays (using `time_sleep` resource) to the GCP resources when creating them via Terraform.
## :wrench: Working with Terraform

Expand Down
1 change: 0 additions & 1 deletion modules/folders/variables.tf
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
variable "folder_name" {}
variable "org_id" {}
variable "dev_folder_id" {}
6 changes: 3 additions & 3 deletions modules/network/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ resource "google_compute_route" "default_to_internet" {
}

# Static public IP address
resource "google_compute_address" "Public_nat" {
name = "publicnat"
resource "google_compute_address" "static_ip" {
name = "static-ip"
address_type = "EXTERNAL"
network_tier = "PREMIUM"
}

# Firewall rules
resource "google_compute_firewall" "ssh_rule" {
resource "google_compute_firewall" "firewall" {
name = "ssh-firewall"
network = google_compute_network.vpc.name
allow {
Expand Down
4 changes: 2 additions & 2 deletions modules/network/output.tf
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
output "public_subnet_name" {
value = google_compute_subnetwork.public_subnet.name
output "static_ip" {
value = google_compute_address.static_ip.address
}
16 changes: 16 additions & 0 deletions modules/os_login/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/os_login_ssh_public_key
data "google_client_openid_userinfo" "me" {}

# Add public ssh key to IAM user
resource "google_os_login_ssh_public_key" "cache" {
user = data.google_client_openid_userinfo.me.email
key = file("~/.ssh/gcp-compute.pub")
}

# Allow IAM user to use OS Login
# If you are project owner or editor, this role is configured automatically.
resource "google_project_iam_member" "project" {
project = var.project_id
role = "roles/compute.osAdminLogin"
member = "user:${data.google_client_openid_userinfo.me.email}"
}
1 change: 1 addition & 0 deletions modules/os_login/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
variable "project_id" {}
2 changes: 1 addition & 1 deletion modules/projects/main.tf
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
resource "google_project" "vpc_project" {
resource "google_project" "gke_project" {
name = var.project_name
project_id = var.project_id
folder_id = "folders/${var.gke_folder_id}"
Expand Down
2 changes: 0 additions & 2 deletions modules/projects/variables.tf
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
variable "region" {}
variable "project_name" {}
variable "project_id" {}
# variable "folder_name" {}
variable "org_id" {}
variable "billing_account_id" {}
variable "gke_folder_id" {}
4 changes: 0 additions & 4 deletions modules/services/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,4 @@ resource "google_project_service" "project_apis" {
project = var.project_id
service = each.key
disable_on_destroy = false
timeouts {
create = "30m"
update = "40m"
}
}
9 changes: 4 additions & 5 deletions modules/vm/main.tf
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
resource "google_compute_instance" "csye7125_vm" {
resource "google_compute_instance" "vm" {
name = var.vm_name
machine_type = var.machine_type
zone = var.zone

# tags = ["csye7125", "vm", "dev"]

boot_disk {
initialize_params {
image = "debian-cloud/debian-11"
Expand All @@ -20,10 +18,11 @@ resource "google_compute_instance" "csye7125_vm" {
access_config {
# ephimeral public IP config
# https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_instance#nested_access_config
nat_ip = var.static_ip
}
}
metadata = {
os = "debian-11"
type = "compute-instance"
# Enable os-login through metadata
enable-oslogin : "TRUE"
}
}
4 changes: 1 addition & 3 deletions modules/vm/variables.tf
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
variable "vm_name" {}
variable "vpc_name" {}
variable "machine_type" {}
variable "region" {}
variable "zone" {}
variable "subnetwork" {}
variable "subnet_name" {}
variable "static_ip" {}
17 changes: 12 additions & 5 deletions root/main.tf
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
module "folders" {
source = "../modules/folders"
folder_name = var.folder_name
org_id = var.org_id
dev_folder_id = var.dev_folder_id
}

Expand All @@ -18,7 +17,6 @@ module "projects" {
project_name = var.project_name
org_id = var.org_id
billing_account_id = var.billing_account_id
region = var.region
}

resource "time_sleep" "creating_projects" {
Expand Down Expand Up @@ -57,10 +55,19 @@ module "vm" {
depends_on = [time_sleep.creating_network]
source = "../modules/vm"
vm_name = var.vm_name
vpc_name = var.vpc_name
subnet_name = var.subnet_name
machine_type = var.machine_type
region = var.region
zone = var.zone
subnetwork = module.network.public_subnet_name
static_ip = module.network.static_ip
}

resource "time_sleep" "creating_vm" {
depends_on = [module.vm]
create_duration = "60s"
}

module "os_login" {
depends_on = [time_sleep.creating_vm]
source = "../modules/os_login"
project_id = var.project_id
}
29 changes: 15 additions & 14 deletions root/variables.tf
Original file line number Diff line number Diff line change
@@ -1,74 +1,75 @@
variable "region" {
description = "VPC region"
default = "us-east1"
type = string
description = "VPC region"
}

variable "dev_folder_id" {
description = "Dev folder ID in organization"
default = "135331753386"
type = string
description = "Dev folder ID in organization"
}

variable "folder_name" {
description = "GCP organization folder name"
default = "test-folder"
type = string
description = "GCP organization folder name"
}

variable "project_name" {
description = "GCP project display name"
default = "tf-gcp-org"
type = string
description = "GCP project display name"
}

# Follows regex: /^[a-z][-a-z0-9]{4,28}[a-z0-9]{1}$/gm
variable "project_id" {
description = "GCP Project ID"
default = "tf-gcp-org-id"
type = string
description = "must be between 6 and 30 characters and can have lowercase letters, digits, or hyphens.It must start with a lowercase letter and end with a letter or number."
}

variable "org_id" {
description = "The numeric ID of the organization this project belongs to"
type = string
description = "The numeric ID of the organization this project belongs to"
}

variable "billing_account_id" {
description = "The alphanumeric ID of the billing account this project belongs to"
type = string
description = "The alphanumeric ID of the billing account this project belongs to"
}

variable "api_names" {
description = "list of apis to enable"
type = list(string)
description = "list of apis to enable"
}

variable "vpc_name" {
description = "VPC name"
type = string
description = "VPC name"
}

variable "subnet_cidr" {
description = "list of subnet cidr"
type = list(string)
description = "list of subnet cidr"
}

variable "subnet_name" {
description = "Subnet name"
type = string
description = "Subnet name"
}

variable "vm_name" {
description = "VM name"
type = string
description = "VM name"
}

variable "machine_type" {
description = "VM name"
type = string
description = "VM name"
}

variable "zone" {
description = "Zone name"
type = string
description = "Zone name"
}

0 comments on commit c34d8d8

Please sign in to comment.