Merge pull request #541 from apichick/workload-identity-federation

Workload identity federation example
This commit is contained in:
Julio Castillo 2022-02-16 12:34:44 +01:00 committed by GitHub
commit 59aecb0229
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 445 additions and 0 deletions

View File

@ -0,0 +1,96 @@
# Configuring workload identity federation to access Google Cloud resources from apps running on Azure
The most straightforward way for workloads running outside of Google Cloud to call Google Cloud APIs is by using a downloaded service account key. However, this approach has 2 major pain points:
* A management hassle, keys need to be stored securely and rotated often.
* A security risk, keys are long term credentials that could be compromised.
Workload identity federation enables applications running outside of Google Cloud to replace long-lived service account keys with short-lived access tokens. This is achieved by configuring Google Cloud to trust an external identity provider, so applications can use the credentials issued by the external identity provider to impersonate a service account.
This example shows how to set up everything, both in Azure and Google Cloud, so a workload in Azure can access Google Cloud resources without a service account key. This will be possible by configuring workload identity federation to trust access tokens generated for a specific application in an Azure Active Directory (AAD) tenant.
The following diagram illustrates how the VM will get a short-lived access token and use it to access a resource:
![Sequence diagram](sequence_diagram.png)
The provided terraform configuration will set up the following architecture:
![Architecture](architecture.png)
* On Azure:
* An Azure Active Directory application and a service principal. By default, the new application grants all users in the Azure AD tenant permission to obtain access tokens. So an app role assignment will be required to restrict which identities can obtain access tokens for the application.
* Optionally, all the resources required to have a VM configured to run with a system-assigned managed identity and accessible via SSH on a public IP using public key authentication, so we can log in to the machine and run the `gcloud` command to verify that everything works as expected.
* On Google Cloud:
* A Google Cloud project with:
* A workload identity pool and provider configured to trust the AAD application
* A service account with the Viewer role granted on the project. The external identities in the workload identity pool would be assigned the Workload Identity User role on that service account.
## Running the example
Clone this repository or [open it in cloud shell](https://ssh.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https%3A%2F%2Fgithub.com%2Fterraform-google-modules%2Fcloud-foundation-fabric&cloudshell_print=cloud-shell-readme.txt&cloudshell_working_dir=examples%2Fcloud-operations%2Fworkload-identity-federation), then go through the following steps to create resources:
* `terraform init`
* `terraform apply -var project_id=my-project-id`
## Testing the example
Once the resources have been created, do the following to verify that everything works as expected:
1. Log in to the VM.
If you have created the VM using this terraform configuration proceed the following way:
* Copy the public IP address of the Azure VM and the username required to log in to the VM via SSH from the output.
* Save the private key to a file
`terraform state pull | jq -r '.outputs.tls_private_key.value' > private_key.pem`
* Change the permissions on the private key file to 600
`chmod 600 private_key.pem`
* Login to the Azure VM using the following command:
`ssh -i private_key.pem azureuser@VM_PUBLIC_IP`
If you already had an existing VM with the gcloud CLI installed that you want to use, you will have assign its managed identity an application role as explained [here](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-assign-app-role-managed-identity-powershell#assign-a-managed-identity-access-to-another-applications-app-role).
2. Create a file called credential.json in the VM with the contents of the `credential` output.
3. Authorize gcloud to access Google Cloud with the credentials file created in the step before.
`gcloud auth login --cred-file credential.json
4. Get the Google Cloud project details
`gcloud projects describe PROJECT_ID`
Once done testing, you can clean up resources by running `terraform destroy`.
<!-- BEGIN TFDOC -->
## Variables
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [project_id](variables.tf#L26) | Identifier of the project that will contain the Pub/Sub topic that will be created from Azure and the service account that will be impersonated. | <code>string</code> | ✓ | |
| [project_create](variables.tf#L17) | Parameters for the creation of the new project. | <code title="object&#40;&#123;&#10; billing_account_id &#61; string&#10; parent &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [vm_test](variables.tf#L31) | Flag indicating whether the infrastructure required to test that everything works should be created in Azure. | <code>bool</code> | | <code>false</code> |
## Outputs
| name | description | sensitive |
|---|---|:---:|
| [credential](outputs.tf#L17) | Credential configuration file contents. | |
| [tls_private_key](outputs.tf#L28) | Private key required to log in to the Azure VM via SSH. | ✓ |
| [username](outputs.tf#L34) | Username required to log in to the Azure VM via SSH. | |
| [vm_public_ip_address](outputs.tf#L39) | Azure VM public IP address. | |
<!-- END TFDOC -->

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@ -0,0 +1,165 @@
/**
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
locals {
username = "azureuser"
app_name = "test-app"
}
provider "azurerm" {
features {}
}
data "azuread_client_config" "config" {}
resource "azuread_application" "app" {
display_name = local.app_name
identifier_uris = ["api://${local.app_name}"]
app_role {
allowed_member_types = ["Application"]
description = "User"
display_name = "User"
enabled = true
id = "3a9a28d5-f98d-47f5-ad0b-07d919533886"
value = "user"
}
}
resource "azuread_service_principal" "service_principal" {
application_id = azuread_application.app.application_id
app_role_assignment_required = true
}
resource "azurerm_resource_group" "resource_group" {
count = var.vm_test ? 1 : 0
name = "resourceGroup"
location = "West Europe"
}
resource "azurerm_virtual_network" "vnet" {
count = var.vm_test ? 1 : 0
name = "vnet"
address_space = ["10.0.0.0/16"]
resource_group_name = azurerm_resource_group.resource_group[0].name
location = azurerm_resource_group.resource_group[0].location
}
resource "azurerm_subnet" "subnet" {
count = var.vm_test ? 1 : 0
name = "subnet"
resource_group_name = azurerm_resource_group.resource_group[0].name
virtual_network_name = azurerm_virtual_network.vnet[0].name
address_prefixes = ["10.0.1.0/24"]
}
resource "azurerm_public_ip" "public_ip" {
count = var.vm_test ? 1 : 0
name = "vm"
resource_group_name = azurerm_resource_group.resource_group[0].name
location = azurerm_resource_group.resource_group[0].location
allocation_method = "Static"
sku = "Basic"
}
resource "azurerm_network_interface" "nic" {
count = var.vm_test ? 1 : 0
name = "nic"
resource_group_name = azurerm_resource_group.resource_group[0].name
location = azurerm_resource_group.resource_group[0].location
ip_configuration {
name = "ipconfig"
subnet_id = azurerm_subnet.subnet[0].id
private_ip_address_allocation = "Dynamic"
public_ip_address_id = azurerm_public_ip.public_ip[0].id
}
}
resource "azurerm_network_security_group" "security_group" {
count = var.vm_test ? 1 : 0
name = "security-group"
resource_group_name = azurerm_resource_group.resource_group[0].name
location = azurerm_resource_group.resource_group[0].location
security_rule {
name = "SSH"
priority = 1001
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = "*"
destination_address_prefix = "*"
}
}
resource "azurerm_network_interface_security_group_association" "security_group_association" {
count = var.vm_test ? 1 : 0
network_interface_id = azurerm_network_interface.nic[0].id
network_security_group_id = azurerm_network_security_group.security_group[0].id
}
resource "tls_private_key" "private_key" {
count = var.vm_test ? 1 : 0
algorithm = "RSA"
rsa_bits = 4096
}
resource "azurerm_linux_virtual_machine" "vm" {
count = var.vm_test ? 1 : 0
name = "vm"
resource_group_name = azurerm_resource_group.resource_group[0].name
location = azurerm_resource_group.resource_group[0].location
network_interface_ids = [azurerm_network_interface.nic[0].id]
size = "Standard_DS1_v2"
os_disk {
name = "disk"
caching = "ReadWrite"
storage_account_type = "Premium_LRS"
}
source_image_reference {
publisher = "Canonical"
offer = "UbuntuServer"
sku = "18.04-LTS"
version = "latest"
}
computer_name = "vm"
admin_username = "azureuser"
disable_password_authentication = true
custom_data = filebase64("${path.module}/setup.sh")
admin_ssh_key {
username = local.username
public_key = tls_private_key.private_key[0].public_key_openssh
}
identity {
type = "SystemAssigned"
}
}
resource "azuread_app_role_assignment" "app_role_assignment" {
count = var.vm_test ? 1 : 0
app_role_id = azuread_application.app.app_role_ids["user"]
principal_object_id = azurerm_linux_virtual_machine.vm[0].identity[0].principal_id
resource_object_id = azuread_service_principal.service_principal.object_id
}

View File

@ -0,0 +1,17 @@
{
"type": "external_account",
"audience": "//iam.googleapis.com/projects/${project_number}/locations/global/workloadIdentityPools/${pool_id}/providers/${provider_id}",
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
"token_url": "https://sts.googleapis.com/v1/token",
"credential_source": {
"url": "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=${app_id_uri}",
"headers": {
"Metadata": "True"
},
"format": {
"type": "json",
"subject_token_field_name": "access_token"
}
},
"service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${service_account_email}:generateAccessToken"
}

View File

@ -0,0 +1,69 @@
/**
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
module "prj" {
source = "../../../modules/project"
billing_account = (var.project_create != null
? var.project_create.billing_account_id
: null
)
parent = (var.project_create != null
? var.project_create.parent
: null
)
name = var.project_id
services = [
"cloudresourcemanager.googleapis.com",
"iam.googleapis.com",
"iamcredentials.googleapis.com",
"sts.googleapis.com",
]
project_create = var.project_create != null
iam_additive = {
"roles/viewer" : [module.sa.iam_email]
}
}
resource "google_iam_workload_identity_pool" "pool" {
provider = google-beta
project = module.prj.project_id
workload_identity_pool_id = "test-pool"
}
resource "google_iam_workload_identity_pool_provider" "provider" {
provider = google-beta
project = module.prj.project_id
workload_identity_pool_id = google_iam_workload_identity_pool.pool.workload_identity_pool_id
workload_identity_pool_provider_id = "test-provider"
attribute_mapping = {
"google.subject" = "assertion.sub"
}
oidc {
allowed_audiences = ["api://${local.app_name}"]
issuer_uri = "https://sts.windows.net/${data.azuread_client_config.config.tenant_id}"
}
}
module "sa" {
source = "../../../modules/iam-service-account"
project_id = module.prj.project_id
name = "sa-test"
iam = {
"roles/iam.workloadIdentityUser" = [
"principalSet://iam.googleapis.com/projects/${module.prj.number}/locations/global/workloadIdentityPools/${google_iam_workload_identity_pool.pool.workload_identity_pool_id}/*"
]
}
}

View File

@ -0,0 +1,42 @@
/**
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
output "credential" {
description = "Credential configuration file contents."
value = templatefile("${path.module}/credential.json", {
project_number = module.prj.project_id
app_id_uri = "api://${local.app_name}"
pool_id = google_iam_workload_identity_pool.pool.workload_identity_pool_id
provider_id = google_iam_workload_identity_pool_provider.provider.workload_identity_pool_provider_id
service_account_email = module.sa.email
})
}
output "tls_private_key" {
description = "Private key required to log in to the Azure VM via SSH."
value = var.vm_test ? tls_private_key.private_key[0].private_key_pem : null
sensitive = true
}
output "username" {
description = "Username required to log in to the Azure VM via SSH."
value = var.vm_test ? local.username : null
}
output "vm_public_ip_address" {
description = "Azure VM public IP address."
value = var.vm_test ? azurerm_public_ip.public_ip[0].ip_address : null
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -0,0 +1,21 @@
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#!/bin/bash
sudo apt -y update
sudo apt -y install apt-transport-https ca-certificates gnupg jq
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list
curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key --keyring /usr/share/keyrings/cloud.google.gpg add -
sudo apt-get update && sudo apt-get install google-cloud-sdk

View File

@ -0,0 +1,35 @@
/**
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
variable "project_create" {
description = "Parameters for the creation of the new project."
type = object({
billing_account_id = string
parent = string
})
default = null
}
variable "project_id" {
description = "Identifier of the project that will contain the Pub/Sub topic that will be created from Azure and the service account that will be impersonated."
type = string
}
variable "vm_test" {
description = "Flag indicating whether the infrastructure required to test that everything works should be created in Azure."
type = bool
default = false
}