Skip to content

fix: address some security concerns #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
function-app.zip
*.terraform
terraform.tfstate*
*.swp
settings.auto.tfvars
.terraform.lock.hcl

function-app.zip
function-app-v1/host.json
function-app-v1/ProviderRelay/function.json

venv
48 changes: 0 additions & 48 deletions function-app-v1/.gitignore

This file was deleted.

12 changes: 0 additions & 12 deletions function-app-v1/ProviderRelay/function.json.tmpl

This file was deleted.

16 changes: 0 additions & 16 deletions function-app-v1/host.json

This file was deleted.

7 changes: 4 additions & 3 deletions function-app-v1/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
# The Python Worker is managed by the Azure Functions platform
# Manually managing azure-functions-worker may cause unexpected issues

azure-functions==1.14.0
azure-identity==1.13.0
boto3==1.26.142
# Core function dependencies - updated for Python 3.12 compatibility
azure-functions==1.23.0
azure-identity==1.23.0
boto3==1.39.2
133 changes: 98 additions & 35 deletions function.tf
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ resource "azurerm_application_insights" "stacklet" {
resource_group_name = azurerm_resource_group.stacklet_rg.name
application_type = "web"
tags = local.tags
retention_in_days = 90
}

resource "azurerm_service_plan" "stacklet" {
Expand All @@ -31,62 +32,124 @@ resource "azurerm_service_plan" "stacklet" {
tags = local.tags
}

# Generate the function.json with queue name
resource "local_file" "function_json" {
content = jsonencode({
scriptFile = "__init__.py"
bindings = [
{
name = "msg"
type = "queueTrigger"
direction = "in"
queueName = azurerm_storage_queue.stacklet.name
connection = "AzureWebJobsStorage"
}
]
})
filename = "${path.module}/function-app-v1/ProviderRelay/function.json"
}

# Create host.json with enhanced logging configuration
resource "local_file" "host_json" {
content = jsonencode({
version = "2.0"
logging = {
applicationInsights = {
samplingSettings = {
isEnabled = true
excludedTypes = "Request"
}
}
logLevel = {
default = "Information"
"ProviderRelay" = "Information"
"azure.functions" = "Warning"
"azure.storage" = "Warning"
}
}
functions = ["ProviderRelay"]
extensionBundle = {
id = "Microsoft.Azure.Functions.ExtensionBundle"
version = "[4.*, 5.0.0)"
}
})
filename = "${path.module}/function-app-v1/host.json"
}

# Create the function app deployment package
data "archive_file" "function_app" {
depends_on = [local_file.function_json, local_file.host_json]

type = "zip"
source_dir = "${path.module}/function-app-v1"
output_path = "${path.module}/function-app.zip"
}

resource "azurerm_linux_function_app" "stacklet" {
name = "stacklet-${var.prefix}-function-app-${substr(random_string.storage_account_suffix.result, 0, 15)}"
name = "${var.prefix}-relay-app"
resource_group_name = azurerm_resource_group.stacklet_rg.name
location = azurerm_resource_group.stacklet_rg.location

storage_account_name = azurerm_storage_account.stacklet.name
storage_account_access_key = azurerm_storage_account.stacklet.primary_access_key
service_plan_id = azurerm_service_plan.stacklet.id
# replaces storage_account_access_key
# storage_uses_managed_identity = true # Use managed identity instead of access keys

# Deploy from zip file
zip_deploy_file = data.archive_file.function_app.output_path

# Enforce HTTPS and private access
https_only = true
client_certificate_enabled = false
public_network_access_enabled = false

site_config {
application_stack {
python_version = "3.10"
python_version = "3.12"
}

# Security hardening
ftps_state = "Disabled" # Disable FTP/FTPS
http2_enabled = true # Enable HTTP/2
minimum_tls_version = "1.2" # Enforce TLS 1.2+
remote_debugging_enabled = false # Disable remote debugging
scm_minimum_tls_version = "1.2" # SCM also uses TLS 1.2+
websockets_enabled = false # Disable WebSockets
}

app_settings = {
# Build and deployment settings
SCM_DO_BUILD_DURING_DEPLOYMENT = true

# Application Insights
APPINSIGHTS_INSTRUMENTATIONKEY = azurerm_application_insights.stacklet.instrumentation_key
AZURE_CLIENT_ID = azurerm_user_assigned_identity.stacklet_identity.client_id
AZURE_AUDIENCE = local.audience
AZURE_STORAGE_QUEUE_NAME = azurerm_storage_queue.stacklet.name
AZURE_SUBSCRIPTION_ID = data.azurerm_subscription.current.subscription_id
AWS_TARGET_ACCOUNT = var.aws_target_account
AWS_TARGET_REGION = var.aws_target_region
AWS_TARGET_ROLE_NAME = var.aws_target_role_name
AWS_TARGET_PARTITION = var.aws_target_partition
AWS_TARGET_EVENT_BUS = var.aws_target_event_bus

# # Storage connection using managed identity
# AzureWebJobsStorage__accountName = azurerm_storage_account.stacklet.name
# AzureWebJobsStorage__credential = "managedidentity"

# Application configuration
AZURE_CLIENT_ID = azurerm_user_assigned_identity.stacklet_identity.client_id
AZURE_AUDIENCE = local.audience
AZURE_STORAGE_QUEUE_NAME = azurerm_storage_queue.stacklet.name
AWS_TARGET_ACCOUNT = var.aws_target_account
AWS_TARGET_REGION = var.aws_target_region
AWS_TARGET_ROLE_NAME = var.aws_target_role_name
AWS_TARGET_PARTITION = var.aws_target_partition
AWS_TARGET_EVENT_BUS = var.aws_target_event_bus
FUNCTION_SOURCE_HASH = data.archive_file.function_app.output_sha
}

# # Authentication disabled since no HTTP access
# auth_settings {
# enabled = false
# }

identity {
type = "UserAssigned"
identity_ids = [azurerm_user_assigned_identity.stacklet_identity.id]
}
tags = local.tags
}

resource "local_file" "function_json" {
content = templatefile(
"${path.module}/function-app-v1/ProviderRelay/function.json.tmpl", { queue_name = azurerm_storage_queue.stacklet.name })
filename = "${path.module}/function-app-v1/ProviderRelay/function.json"
}


resource "null_resource" "function_deploy" {
depends_on = [azurerm_linux_function_app.stacklet, local_file.function_json]
# ensures that publish always runs
triggers = {
build_number = "${timestamp()}"
}

# initial deployment of the function-app could race with provisioning, so sleep 10 seconds
provisioner "local-exec" {
command = <<EOF
cd ${path.module}/function-app-v1
sleep 10
func azure functionapp publish ${azurerm_linux_function_app.stacklet.name} --python
EOF
}
tags = local.tags
}
39 changes: 38 additions & 1 deletion main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,12 @@ data "azurerm_role_definition" "builtin" {

resource "random_uuid" "app_role_uuid" {}

locals {
resource_group_name = var.resource_group_name == null ? "${var.prefix}-stacklet-relay" : var.resource_group_name
}

resource "azurerm_resource_group" "stacklet_rg" {
name = var.prefix
name = local.resource_group_name
location = var.resource_group_location
tags = local.tags
}
Expand All @@ -40,6 +44,27 @@ resource "azurerm_user_assigned_identity" "stacklet_identity" {
tags = local.tags
}

# # Role assignment for storage queue access
# resource "azurerm_role_assignment" "storage_queue_data_contributor" {
# scope = azurerm_storage_account.stacklet.id
# role_definition_name = "Storage Queue Data Contributor"
# principal_id = azurerm_user_assigned_identity.stacklet_identity.principal_id
# }

# # Role assignment for storage blob access (for function app packages)
# resource "azurerm_role_assignment" "storage_blob_data_contributor" {
# scope = azurerm_storage_account.stacklet.id
# role_definition_name = "Storage Blob Data Contributor"
# principal_id = azurerm_user_assigned_identity.stacklet_identity.principal_id
# }

# # Role assignment for Application Insights
# resource "azurerm_role_assignment" "monitoring_contributor" {
# scope = azurerm_application_insights.stacklet.id
# role_definition_name = "Monitoring Contributor"
# principal_id = azurerm_user_assigned_identity.stacklet_identity.principal_id
# }

resource "azuread_application" "stacklet_application" {
count = var.azuread_application == null ? 1 : 0
display_name = "${var.prefix}-application"
Expand Down Expand Up @@ -85,6 +110,18 @@ data "azuread_service_principal" "stacklet_sp" {
display_name = var.azuread_application
}

# Terraform's Azure AD provider doesn't have a direct resource for app role assignments, so this uses the Azure CLI REST API
# as a workaround to create the assignment that enables the federated identity scenario.
# This block is essential for allowing the Azure Function to authenticate to AWS using Azure managed identity.
#
# Azure Function (Managed Identity)
# ↓ (gets Azure AD token)
# Azure AD App Role Assignment ← This block creates this
# ↓ (token includes app role claim)
# AWS STS AssumeRoleWithWebIdentity
# ↓ (returns AWS credentials)
# AWS EventBridge API

resource "null_resource" "stacklet" {
depends_on = [local.azuread_application, local.azuread_service_principal]
provisioner "local-exec" {
Expand Down
23 changes: 22 additions & 1 deletion provider.tf
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,27 @@
#
# SPDX-License-Identifier: Apache-2.0

# Note: Unlike AWS provider, Azure provider (azurerm) does not support
# default_tags configuration. We use local.tags instead to achieve
# consistent tagging across all resources.
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = ">=4.35.0"
}
}
}

provider "azurerm" {
features {}
features {
resource_group {
prevent_deletion_if_contains_resources = !var.force_delete_resource_group
}
}

subscription_id = var.subscription_id

# Use Azure AD authentication for storage operations (required when access keys are disabled)
storage_use_azuread = true
}
Loading