diff --git a/servers/Azure.Mcp.Server/changelog-entries/secure-creation-defaults-deploy.yaml b/servers/Azure.Mcp.Server/changelog-entries/secure-creation-defaults-deploy.yaml new file mode 100644 index 0000000000..6edc9c91bc --- /dev/null +++ b/servers/Azure.Mcp.Server/changelog-entries/secure-creation-defaults-deploy.yaml @@ -0,0 +1,3 @@ +changes: + - section: Bugs Fixed + description: Deployment IaC guidance now defaults to private networking, workload identity, and least-privilege application roles. Separate opt-in parameters permit public networking, all-Azure-services firewall exceptions, privileged application roles, and secret-based connections. \ No newline at end of file diff --git a/servers/Azure.Mcp.Server/docs/azmcp-commands.md b/servers/Azure.Mcp.Server/docs/azmcp-commands.md index e8071fcb00..d99dc32bba 100644 --- a/servers/Azure.Mcp.Server/docs/azmcp-commands.md +++ b/servers/Azure.Mcp.Server/docs/azmcp-commands.md @@ -2798,7 +2798,11 @@ azmcp deploy architecture diagram generate --raw-mcp-tool-input # ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired azmcp deploy iac rules get --deployment-tool \ [--iac-type ] \ - [--resource-types ] + [--resource-types ] \ + [--enable-public-network-access ] \ + [--allow-azure-services ] \ + [--allow-privileged-roles ] \ + [--use-connection-strings ] # Get the ci/cd pipeline guidance # ❌ Destructive | ✅ Idempotent | ❌ OpenWorld | ✅ ReadOnly | ❌ Secret | ❌ LocalRequired diff --git a/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md b/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md index 48d5ae9983..de81370d24 100644 --- a/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md +++ b/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md @@ -630,6 +630,14 @@ The `Interaction` column describes whether a prompt can invoke its tool immediat | deploy_app_logs_get | Show me the log of the application deployed by azd | context-required | | deploy_architecture_diagram_generate | Generate the Azure architecture diagram for this application | context-required | | deploy_iac_rules_get | Give me the IaC rules for deploying this application to Azure Container Apps using Azure CLI and Bicep | none | +| deploy_iac_rules_get | Get Azure CLI and Bicep rules for AKS, Cosmos DB, Storage, and Key Vault with private networking, workload identity, and least-privilege runtime roles | none | +| deploy_iac_rules_get | Get Azure CLI and Bicep rules for AKS, Cosmos DB, and Key Vault; explicitly permit public networking, all-Azure-services firewall exceptions, privileged application roles, and secret-based connections for this isolated test | none | +| deploy_iac_rules_get | Generate Bicep rules for a Function App with least-privilege storage roles and no owner-level application role | none | +| deploy_iac_rules_get | Generate Terraform rules for a Function App and explicitly allow the Storage Blob Data Owner application role | none | +| deploy_iac_rules_get | Get PostgreSQL and MySQL deployment rules using managed identity authentication without passwords or connection strings | none | +| deploy_iac_rules_get | Get PostgreSQL deployment rules and explicitly allow secret-based connection strings stored in Key Vault | none | +| deploy_iac_rules_get | Generate Azure SQL Database rules with public network access disabled and a private endpoint | none | +| deploy_iac_rules_get | Generate Azure SQL Database rules and explicitly permit public networking for approved client IPs | none | | deploy_pipeline_guidance_get | Generate a CI/CD pipeline using GitHub Actions workflow to deploy my application to Azure with best practices | context-required | | deploy_plan_get | Generate an Azure deployment plan for this project using Azure CLI and IaC templates | context-required | diff --git a/tools/Azure.Mcp.Tools.Deploy/cspell.yaml b/tools/Azure.Mcp.Tools.Deploy/cspell.yaml index 119daa86b6..a19c19b686 100644 --- a/tools/Azure.Mcp.Tools.Deploy/cspell.yaml +++ b/tools/Azure.Mcp.Tools.Deploy/cspell.yaml @@ -38,6 +38,7 @@ words: - mycontainerapp - mylog - passwordless + - privatelink - rgabcde - serviceconnector - sqlabcde diff --git a/tools/Azure.Mcp.Tools.Deploy/src/Commands/Infrastructure/RulesGetCommand.cs b/tools/Azure.Mcp.Tools.Deploy/src/Commands/Infrastructure/RulesGetCommand.cs index b8b2fa90eb..79d0a4a2e0 100644 --- a/tools/Azure.Mcp.Tools.Deploy/src/Commands/Infrastructure/RulesGetCommand.cs +++ b/tools/Azure.Mcp.Tools.Deploy/src/Commands/Infrastructure/RulesGetCommand.cs @@ -43,7 +43,11 @@ public override Task ExecuteAsync(CommandContext context, Rules string iacRules = IaCRulesTemplateUtil.GetIaCRules( options.DeploymentTool, options.IacType ?? string.Empty, - resourceTypes ?? []); + resourceTypes ?? [], + options.EnablePublicNetworkAccess, + options.AllowAzureServices, + options.AllowPrivilegedRoles, + options.UseConnectionStrings); context.Response.Message = iacRules; } diff --git a/tools/Azure.Mcp.Tools.Deploy/src/Models/Templates/IaCRulesTemplateParameters.cs b/tools/Azure.Mcp.Tools.Deploy/src/Models/Templates/IaCRulesTemplateParameters.cs index 905f943dcd..d2c212f0f3 100644 --- a/tools/Azure.Mcp.Tools.Deploy/src/Models/Templates/IaCRulesTemplateParameters.cs +++ b/tools/Azure.Mcp.Tools.Deploy/src/Models/Templates/IaCRulesTemplateParameters.cs @@ -18,6 +18,10 @@ public sealed class IaCRulesTemplateParameters public string FinalInstructions { get; set; } = string.Empty; public string RequiredTools { get; set; } = string.Empty; public string AdditionalNotes { get; set; } = string.Empty; + public bool EnablePublicNetworkAccess { get; set; } + public bool AllowAzureServices { get; set; } + public bool AllowPrivilegedRoles { get; set; } + public bool UseConnectionStrings { get; set; } /// /// Converts the parameters to a dictionary for template processing. diff --git a/tools/Azure.Mcp.Tools.Deploy/src/Options/Infrastructure/RulesGetOptions.cs b/tools/Azure.Mcp.Tools.Deploy/src/Options/Infrastructure/RulesGetOptions.cs index ff6c822d88..e4e39411d5 100644 --- a/tools/Azure.Mcp.Tools.Deploy/src/Options/Infrastructure/RulesGetOptions.cs +++ b/tools/Azure.Mcp.Tools.Deploy/src/Options/Infrastructure/RulesGetOptions.cs @@ -15,4 +15,16 @@ public sealed class RulesGetOptions [Option(Description = "Comma-separated list of Azure resource types to generate rules for. Get the value from context and use the same resources defined in plan. Valid value: 'appservice', 'containerapp', 'function', 'aks', 'azuredatabaseforpostgresql', 'azuredatabaseformysql', 'azuresqldatabase', 'azurecosmosdb', 'azurestorageaccount', 'azurekeyvault'")] public string? ResourceTypes { get; set; } + + [Option(Description = "Generate guidance permitting public network access (less secure). Defaults to false; private networking is recommended otherwise.")] + public bool EnablePublicNetworkAccess { get; set; } + + [Option(Description = "Permit database firewall access from all Azure services, including other customers (insecure). Requires --enable-public-network-access. Defaults to false.")] + public bool AllowAzureServices { get; set; } + + [Option(Description = "Recommend elevated application permissions such as Key Vault Secrets Officer (broad access). Defaults to false; least-privilege runtime roles are used otherwise.")] + public bool AllowPrivilegedRoles { get; set; } + + [Option(Description = "Use secret-based connection strings instead of workload or managed identity where needed (less secure). Defaults to false; secrets must still be stored securely.")] + public bool UseConnectionStrings { get; set; } } diff --git a/tools/Azure.Mcp.Tools.Deploy/src/Services/Util/IaCRulesTemplateUtil.cs b/tools/Azure.Mcp.Tools.Deploy/src/Services/Util/IaCRulesTemplateUtil.cs index de303fca72..41d24eafe3 100644 --- a/tools/Azure.Mcp.Tools.Deploy/src/Services/Util/IaCRulesTemplateUtil.cs +++ b/tools/Azure.Mcp.Tools.Deploy/src/Services/Util/IaCRulesTemplateUtil.cs @@ -12,8 +12,6 @@ namespace Azure.Mcp.Tools.Deploy.Services.Util; /// public static class IaCRulesTemplateUtil { - private static readonly string _databaseCommonRules = TemplateService.LoadTemplate("IaCRules/database-common-rules"); - /// /// Generates IaC rules using embedded templates. /// @@ -21,9 +19,19 @@ public static class IaCRulesTemplateUtil /// The IaC type (bicep, terraform). /// Array of resource types. /// A formatted IaC rules string. - public static string GetIaCRules(string deploymentTool, string iacType, string[] resourceTypes) + public static string GetIaCRules(string deploymentTool, string iacType, string[] resourceTypes, + bool enablePublicNetworkAccess = false, bool allowAzureServices = false, + bool allowPrivilegedRoles = false, bool useConnectionStrings = false) { + if (allowAzureServices && !enablePublicNetworkAccess) + { + throw new ArgumentException("--allow-azure-services requires --enable-public-network-access."); + } var parameters = CreateTemplateParameters(deploymentTool, iacType, resourceTypes); + parameters.EnablePublicNetworkAccess = enablePublicNetworkAccess; + parameters.AllowAzureServices = allowAzureServices; + parameters.AllowPrivilegedRoles = allowPrivilegedRoles; + parameters.UseConnectionStrings = useConnectionStrings; // Default values for optional parameters if (deploymentTool.Equals(DeploymentTool.Azd, StringComparison.OrdinalIgnoreCase) && string.IsNullOrWhiteSpace(iacType)) { @@ -127,7 +135,12 @@ private static string GenerateResourceSpecificRules(IaCRulesTemplateParameters p if (parameters.ResourceTypes.Contains(AzureServiceNames.AzureDatabaseForMySql, StringComparer.OrdinalIgnoreCase)) { - rules.Add(GenerateMySqlRules()); + rules.Add(GenerateMySqlRules(parameters)); + } + + if (parameters.ResourceTypes.Contains(AzureServiceNames.AzureSqlDatabase, StringComparer.OrdinalIgnoreCase)) + { + rules.Add(GenerateAzureSqlRules(parameters)); } if (parameters.ResourceTypes.Contains(AzureServiceNames.AzureCosmosDb, StringComparer.OrdinalIgnoreCase)) @@ -176,8 +189,16 @@ private static string GenerateAppServiceRules(IaCRulesTemplateParameters paramet private static string GenerateFunctionAppRules(IaCRulesTemplateParameters parameters) { - var bicepRules = TemplateService.LoadTemplate("IaCRules/functionapp-bicep-rules"); - var tfRules = TemplateService.LoadTemplate("IaCRules/functionapp-tf-rules"); + var bicepRules = TemplateService.ProcessTemplate("IaCRules/functionapp-bicep-rules", new Dictionary { + { "PrivilegedRoleRule", parameters.AllowPrivilegedRoles + ? "- MANDATORY: Add a Microsoft.Authorization/roleAssignments resource to assign the Storage Blob Data Owner (b7e6dc6d-f1e8-4753-8033-0f276bb0955b) role to the user-assigned managed identity" + : string.Empty } + }); + var tfRules = TemplateService.ProcessTemplate("IaCRules/functionapp-tf-rules", new Dictionary { + { "PrivilegedRoleRule", parameters.AllowPrivilegedRoles + ? "- MANDATORY: Add a azurerm_role_assignment resource to assign the Storage Blob Data Owner (b7e6dc6d-f1e8-4753-8033-0f276bb0955b) role to the user-assigned managed identity" + : string.Empty } + }); return TemplateService.ProcessTemplate("IaCRules/functionapp-rules", new Dictionary { { "ToolSpecificRules", GetToolSpecificResourceRules(parameters.IacType, bicepRules, tfRules, null)} }); @@ -190,7 +211,12 @@ private static string GenerateAKSRules(IaCRulesTemplateParameters parameters) var cliRules = TemplateService.LoadTemplate("IaCRules/aks-cli-rules"); return TemplateService.ProcessTemplate("IaCRules/aks-rules", new Dictionary { { "ToolSpecificRules", GetToolSpecificResourceRules(parameters.IacType, bicepRules, tfRules, cliRules)}, - { "KeyvaultIntegrationRules", TemplateService.LoadTemplate("IaCRules/aks-kv-integration-rules") } + { "AuthenticationRules", parameters.UseConnectionStrings + ? "- Secret-based connection strings are explicitly permitted. Store credentials in Key Vault and consume them through secret references; never plaintext configuration." + : "- Use Azure Workload Identity for AKS connections to Azure services. Enable the OIDC issuer, workload identity, and a dedicated Kubernetes service account with federated credentials and least-privilege data roles." }, + { "KeyvaultIntegrationRules", parameters.UseConnectionStrings + ? TemplateService.LoadTemplate("IaCRules/aks-kv-integration-rules") + : "- Access Key Vault using the workload identity and Key Vault Secrets User permissions. Do not share node identity credentials with application pods." } }); } @@ -199,33 +225,72 @@ private static string GeneratePostgreSqlRules(IaCRulesTemplateParameters paramet var versionRules = parameters.IacType.Equals(IacType.Terraform, StringComparison.OrdinalIgnoreCase) ? "- PostgreSQL SKU name format: B_Standard_B1ms(Burstable tier), GP_Standard_D2s_v3(GeneralPurpose), MO_Standard_E4s_v3(MemoryOptimized)\n- For version, prefer to use '16'." : "For version, use '17' or higher."; - var cliRules = "- If PostgreSQL server uses Azure AD authentication, use '--microsoft-entra-auth Enabled' when creating.\n- Azure CLI uses parameters '--name --rule-name ' for firewall rules creation.\n- IMPORTANT: **If using Azure AD authentication, you MUST ADD a database USER for the managed identity and GRANT all privileges to make the connection work.** Use 'az postgres flexible-server execute' command to run SQL commands to create the user and grant privileges."; + var cliRules = parameters.AllowPrivilegedRoles + ? "- Elevated database privileges are explicitly permitted. Create the managed identity database user and grant the requested privileges only within the application's database." + : "- Create a database user for the managed identity. Grant only CONNECT, required schema USAGE, and the table operations required by the application; do not grant ALL privileges by default."; return TemplateService.ProcessTemplate("IaCRules/postgresql-rules", new Dictionary { { "VersionRules", versionRules }, - { "DatabaseCommonRules", _databaseCommonRules}, + { "DatabaseCommonRules", GenerateDatabaseCommonRules(parameters)}, { "ToolSpecificRules", GetToolSpecificResourceRules(parameters.IacType, null, null, cliRules)} }); } - private static string GenerateMySqlRules() => - TemplateService.ProcessTemplate("IaCRules/mysql-rules", new Dictionary { { "DatabaseCommonRules", _databaseCommonRules } }); + private static string GenerateMySqlRules(IaCRulesTemplateParameters parameters) => + TemplateService.ProcessTemplate("IaCRules/mysql-rules", new Dictionary { { "DatabaseCommonRules", GenerateDatabaseCommonRules(parameters) } }); + + private static string GenerateAzureSqlRules(IaCRulesTemplateParameters parameters) => + TemplateService.ProcessTemplate("IaCRules/azuresql-rules", new Dictionary { + { "DatabaseCommonRules", GenerateDatabaseCommonRules(parameters) }, + { "SqlNetworkRules", parameters.EnablePublicNetworkAccess + ? "- Set publicNetworkAccess to Enabled on the Azure SQL logical server only when public networking is explicitly requested. Restrict server firewall access to approved IPs." + : "- Set publicNetworkAccess to Disabled on the Azure SQL logical server. Configure a private endpoint for the sqlServer subresource and the privatelink.database.windows.net private DNS zone for the application and deployment runner." } + }); + + private static string GenerateDatabaseCommonRules(IaCRulesTemplateParameters parameters) => + TemplateService.ProcessTemplate("IaCRules/database-common-rules", new Dictionary { + { "NetworkRules", GenerateNetworkRules(parameters) }, + { "AuthenticationRules", parameters.UseConnectionStrings + ? "- If username and password are required, you MUST leave them as params.\n- Create secrets in Key Vault to store the connection string or credentials, and assign `Key Vault Secrets User` role to the user-assigned managed identity." + : "- Use Microsoft Entra managed identity and passwordless database authentication. Create a database user mapped to the application identity with only the required permissions; do not request passwords or create connection-string secrets." } + }); + + private static string GenerateNetworkRules(IaCRulesTemplateParameters parameters) => + !parameters.EnablePublicNetworkAccess + ? "- Disable public network access. Configure supported private endpoints or virtual network integration, private DNS, and connectivity for the application and deployment runner." + : parameters.AllowAzureServices + ? "- Public access and the all-Azure-services firewall exception (0.0.0.0) are explicitly permitted for databases. This includes other customers' subscriptions and is not tenant isolation." + : "- Public network access is explicitly permitted. Restrict firewall access to explicitly approved client IPs or subnets; do not add an all-Azure-services exception."; private static string GenerateCosmosDbRules(IaCRulesTemplateParameters parameters) => - TemplateService.ProcessTemplate("IaCRules/cosmosdb-rules", new Dictionary { { "ToolSpecificRules", GetToolSpecificResourceRules(parameters.IacType, null, null, null) } }); + TemplateService.ProcessTemplate("IaCRules/cosmosdb-rules", new Dictionary { { "NetworkRules", GenerateNetworkRules(parameters) } }); private static string GenerateStorageRules(IaCRulesTemplateParameters parameters) { var tfRules = "- Add `storage_use_azuread = true` in azurerm provider."; - return TemplateService.ProcessTemplate("IaCRules/storage-rules", new Dictionary { { "ToolSpecificRules", GetToolSpecificResourceRules(parameters.IacType, null, tfRules, null) } }); + return TemplateService.ProcessTemplate("IaCRules/storage-rules", new Dictionary { + { "ToolSpecificRules", GetToolSpecificResourceRules(parameters.IacType, null, tfRules, null) }, + { "NetworkRules", parameters.EnablePublicNetworkAccess + ? "- Public network access is explicitly permitted; restrict access to approved networks." + : "- Disable public network access and configure private endpoints and private DNS." }, + { "AuthenticationRules", parameters.UseConnectionStrings + ? "- Key authentication is explicitly permitted when required for secret-based connections. Store keys in Key Vault." + : "- Disable storage account local auth (key access); use Microsoft Entra ID." } + }); } private static string GenerateKeyVaultRules(IaCRulesTemplateParameters parameters) { - var bicepRules = "- Allow public access from all networks(set publicNetworkAccess = Enabled)."; + var bicepRules = string.Empty; var tfRules = "- Assign role 'Key Vault Secrets Officer (b86a8fe4-44ce-4948-aee5-eccb2c155cd7)' to current user.This is the dependency for key vault secret creation."; var cliRules = "- IMPORTANT: Assign Key Vault Secrets Officer to current user. Add delay after RBAC role assignment to allow propagation before creating secrets."; - return TemplateService.ProcessTemplate("IaCRules/key-vault-rules", new Dictionary { { "ToolSpecificRules", GetToolSpecificResourceRules(parameters.IacType, bicepRules, tfRules, cliRules) } }); + return TemplateService.ProcessTemplate("IaCRules/key-vault-rules", new Dictionary { + { "ToolSpecificRules", GetToolSpecificResourceRules(parameters.IacType, bicepRules, tfRules, cliRules) }, + { "RuntimeRole", parameters.AllowPrivilegedRoles ? "Key Vault Secrets Officer" : "Key Vault Secrets User" }, + { "NetworkRules", parameters.EnablePublicNetworkAccess + ? "- Public network access is explicitly permitted for Key Vault; restrict its firewall to approved networks." + : "- Set publicNetworkAccess to Disabled for Key Vault. Configure private endpoints and private DNS for both the application and deployment runner." } + }); } /// diff --git a/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/aks-rules.md b/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/aks-rules.md index 12da75d30f..4a41cab744 100644 --- a/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/aks-rules.md +++ b/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/aks-rules.md @@ -2,6 +2,6 @@ - **CRITICAL** PREREQUISITE: You MUST use a stable kubernetesVersion, get by `az aks get-versions --query 'values[?isDefault].version' --location <> -o tsv`. - For SKU, set based on appmod-get-available-region-sku output to get available VM size in the region, e.g. Standard_D2ds_v5. - Node pool names must be 6 characters or less (e.g., 'sys', 'user', 'win01'). -- For AKS connections to other Azure services (storage, databases, etc.) EXCEPT ACR, use connection strings/secrets directly instead of workload identity or service account, etc. +{{AuthenticationRules}} {{ToolSpecificRules}} {{KeyvaultIntegrationRules}} diff --git a/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/azuresql-rules.md b/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/azuresql-rules.md new file mode 100644 index 0000000000..50d4496d8f --- /dev/null +++ b/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/azuresql-rules.md @@ -0,0 +1,4 @@ +=== Additional requirements for Azure SQL Database: +- Apply networking and authentication rules to the Microsoft.Sql/servers logical server hosting the database. +{{DatabaseCommonRules}} +{{SqlNetworkRules}} \ No newline at end of file diff --git a/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/containerapp-bicep-rules.md b/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/containerapp-bicep-rules.md index 9549ea6a8a..ece8031e51 100644 --- a/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/containerapp-bicep-rules.md +++ b/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/containerapp-bicep-rules.md @@ -5,6 +5,5 @@ - Enable CORS via properties.configuration.ingress.corsPolicy. - Define all used secrets; Use Key Vault if possible. - MANDATORY: You MUST add Key Vault secrets and their related role assignment in explicit dependencies to use these secrets. -- IMPORTANT NOTES: If you meet deployment error: "Field 'configuration.secrets' is invalid with details: Invalid value: : unable to get value using managed identity". DO NOT use key vault references first, use connection string directly in container app environment variable and deploy. - After deployment is successful, get IP from the container app, switch to use key vault references in secrets, and 1) add the IP to key vault network rules 2) set defaultAction: 'Deny'. This is a workaround for the issue that managed identity cannot access key vault when key vault has network rules enabled. +- If managed identity cannot resolve a secret, verify private endpoint connectivity, private DNS, and role propagation. Do not bypass network restrictions or put credentials directly in environment variables; use secret references. - Container App Environment must be connected to Log Analytics Workspace. Use logAnalyticsConfiguration -> customerId=logAnalytics.properties.customerId and sharedKey=logAnalytics.listKeys().primarySharedKey. diff --git a/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/containerapp-tf-rules.md b/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/containerapp-tf-rules.md index f178683e15..df87822c0f 100644 --- a/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/containerapp-tf-rules.md +++ b/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/containerapp-tf-rules.md @@ -4,6 +4,5 @@ - Create an ***azapi_resource_action*** resource using :type `Microsoft.App/containerApps`, method `PATCH`, and body `properties.configuration.ingress.corsPolicy` property to enable CORS for all origins, headers, and methods. Use 'azure/azapi' provider version *2.0*. DO NOT use jsonencode() for the body. - Define all used secrets; Use Key Vault if possible. - MANDATORY: You MUST add Key Vault secrets and their related role assignment in explicit dependencies to use these secrets. -- IMPORTANT NOTES: If you meet deployment error: "Field 'configuration.secrets' is invalid with details: Invalid value: : unable to get value using managed identity". DO NOT use key vault references first, use connection string directly in container app environment variable and deploy. - After deployment is successful, get IP from the container app, switch to use key vault references in secrets, and 1) add the IP to key vault network rules 2) set defaultAction: 'Deny'. This is a workaround for the issue that managed identity cannot access key vault when key vault has network rules enabled. +- If managed identity cannot resolve a secret, verify private endpoint connectivity, private DNS, and role propagation. Do not bypass network restrictions or put credentials directly in environment variables; use secret references. - Container App Environment must be connected to Log Analytics Workspace. Use logs_destination="log-analytics" azurerm_container_app_environment.log_analytics_workspace_id = azurerm_log_analytics_workspace..id. diff --git a/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/cosmosdb-rules.md b/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/cosmosdb-rules.md index b1e287ca4b..73e6759981 100644 --- a/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/cosmosdb-rules.md +++ b/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/cosmosdb-rules.md @@ -1,2 +1,2 @@ === Additional requirements for CosmosDB: -- Add firewall rules to allow traffic from Azure Services (allow IP 0.0.0.0). +{{NetworkRules}} diff --git a/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/database-common-rules.md b/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/database-common-rules.md index 6634be9d81..6121a1819b 100644 --- a/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/database-common-rules.md +++ b/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/database-common-rules.md @@ -1,6 +1,5 @@ -- Add firewall rules to allow traffic from Azure Services (allow IP 0.0.0.0). -- If username and password are required, you MUST leave them as params. -- Create secrets in Key Vault to store the connection string or credentials, and assign `Key Vault Secrets User` role to the user-assigned managed identity. +{{NetworkRules}} +{{AuthenticationRules}} - If app used Managed Identity to connect the database, MUST add a post-provision step to use Service Connector create a connection between containerapp and database: 1) Prerequisites: Ensure the Service Connector extension installed in local environment: 'az extension add --name serviceconnector-passwordless'. Check if the app uses user-assigned managed identity or system-assigned managed identity. 2) Run Command: az containerapp connection create mysql-flexible/postgres-flexible/sql --connection connectionname --user-identity client-id=XX subs-id=XX --source-id --tg --server servername --database dbname -y. diff --git a/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/functionapp-bicep-rules.md b/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/functionapp-bicep-rules.md index a4080407b9..1c738e6f1b 100644 --- a/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/functionapp-bicep-rules.md +++ b/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/functionapp-bicep-rules.md @@ -1,4 +1,4 @@ -- MANDATORY: Add a Microsoft.Authorization/roleAssignments resource to assign the Storage Blob Data Owner (b7e6dc6d-f1e8-4753-8033-0f276bb0955b) role to the user-assigned managed identity +{{PrivilegedRoleRule}} - MANDATORY: Add a Microsoft.Authorization/roleAssignments resource to assign the Storage Blob Data Contributor (ba92f5b4-2d11-453d-a403-e96b0029c9fe) role to the user-assigned managed identity - MANDATORY: Add a Microsoft.Authorization/roleAssignments resource to assign the Storage Queue Data Contributor (974c5e8b-45b9-4653-ba55-5f855dd0fb88) role to the user-assigned managed identity - MANDATORY: Add a Microsoft.Authorization/roleAssignments resource to assign the Storage Table Data Contributor (0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3) role to the user-assigned managed identity diff --git a/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/functionapp-tf-rules.md b/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/functionapp-tf-rules.md index 78df4b6a97..88cf69bb93 100644 --- a/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/functionapp-tf-rules.md +++ b/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/functionapp-tf-rules.md @@ -1,4 +1,4 @@ -- MANDATORY: Add a azurerm_role_assignment resource to assign the Storage Blob Data Owner (b7e6dc6d-f1e8-4753-8033-0f276bb0955b) role to the user-assigned managed identity +{{PrivilegedRoleRule}} - MANDATORY: Add a azurerm_role_assignment resource to assign the Storage Blob Data Contributor (ba92f5b4-2d11-453d-a403-e96b0029c9fe) role to the user-assigned managed identity - MANDATORY: Add a azurerm_role_assignment resource to assign the Storage Queue Data Contributor (974c5e8b-45b9-4653-ba55-5f855dd0fb88) role to the user-assigned managed identity - MANDATORY: Add a azurerm_role_assignment resource to assign the Storage Table Data Contributor (0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3) role to the user-assigned managed identity diff --git a/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/key-vault-rules.md b/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/key-vault-rules.md index a05ef5c00e..fe4d004817 100644 --- a/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/key-vault-rules.md +++ b/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/key-vault-rules.md @@ -1,4 +1,5 @@ === Additional requirements for Key Vault: - Use RBAC authentication. -- Assign role 'Key Vault Secrets Officer (b86a8fe4-44ce-4948-aee5-eccb2c155cd7)' to managed identity. Add dependencies to ensure role assignment is finished before application accesses secrets. +- Assign role '{{RuntimeRole}}' to the application managed identity at the application vault scope. Keep provisioning permissions separate from runtime permissions. +{{NetworkRules}} {{ToolSpecificRules}} diff --git a/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/storage-rules.md b/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/storage-rules.md index 9269339f57..fd54e676bf 100644 --- a/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/storage-rules.md +++ b/tools/Azure.Mcp.Tools.Deploy/src/Templates/IaCRules/storage-rules.md @@ -1,4 +1,5 @@ === Additional requirements for Storage Accounts: -- By default, disable storage account local auth (key access). +{{AuthenticationRules}} +{{NetworkRules}} - By default, disable public access to storage blob. {{ToolSpecificRules}} diff --git a/tools/Azure.Mcp.Tools.Deploy/tests/Azure.Mcp.Tools.Deploy.Tests/Commands/Infrastructure/RulesGetCommandTests.cs b/tools/Azure.Mcp.Tools.Deploy/tests/Azure.Mcp.Tools.Deploy.Tests/Commands/Infrastructure/RulesGetCommandTests.cs index cf61ab9e3d..b9fb0bc620 100644 --- a/tools/Azure.Mcp.Tools.Deploy/tests/Azure.Mcp.Tools.Deploy.Tests/Commands/Infrastructure/RulesGetCommandTests.cs +++ b/tools/Azure.Mcp.Tools.Deploy/tests/Azure.Mcp.Tools.Deploy.Tests/Commands/Infrastructure/RulesGetCommandTests.cs @@ -11,6 +11,86 @@ namespace Azure.Mcp.Tools.Deploy.Tests.Commands.Infrastructure; public class RulesGetCommandTests : CommandUnitTestsBase { + [Theory] + [InlineData("bicep")] + [InlineData("terraform")] + public async Task SecurityGuidance_DefaultsToPrivateAndLeastPrivilege(string iacType) + { + var result = await ExecuteCommandAsync("--deployment-tool", "AzCli", "--iac-type", iacType, + "--resource-types", "aks,azurecosmosdb,azuredatabaseforpostgresql,azuredatabaseformysql,azurestorageaccount"); + Assert.Equal(HttpStatusCode.OK, result.Status); + Assert.Contains("Disable public network access", result.Message); + Assert.Contains("'Key Vault Secrets User' to the application managed identity", result.Message); + Assert.Contains("Use Azure Workload Identity", result.Message); + Assert.DoesNotContain("0.0.0.0", result.Message); + Assert.DoesNotContain("{{", result.Message); + } + + [Fact] + public async Task SecurityGuidance_OnlyRelaxesExplicitlyEnabledSettings() + { + var result = await ExecuteCommandAsync("--deployment-tool", "AzCli", "--iac-type", "bicep", + "--resource-types", "aks,azurecosmosdb", "--enable-public-network-access", "true", + "--allow-azure-services", "true", "--allow-privileged-roles", "true", "--use-connection-strings", "true"); + Assert.Equal(HttpStatusCode.OK, result.Status); + Assert.Contains("0.0.0.0", result.Message); + Assert.Contains("'Key Vault Secrets Officer' to the application managed identity", result.Message); + Assert.Contains("Secret-based connection strings are explicitly permitted", result.Message); + Assert.DoesNotContain("{{", result.Message); + } + + [Theory] + [InlineData("azuredatabaseforpostgresql")] + [InlineData("azuredatabaseformysql")] + public async Task DatabaseCredentials_RequireExplicitOptIn(string resourceType) + { + var secure = await ExecuteCommandAsync("--deployment-tool", "AzCli", "--iac-type", "bicep", + "--resource-types", resourceType); + + Assert.Equal(HttpStatusCode.OK, secure.Status); + Assert.Contains("Use Microsoft Entra managed identity and passwordless database authentication", secure.Message); + Assert.DoesNotContain("If username and password are required", secure.Message); + Assert.DoesNotContain("Create secrets in Key Vault to store the connection string", secure.Message); + + var optedIn = await ExecuteCommandAsync("--deployment-tool", "AzCli", "--iac-type", "bicep", + "--resource-types", resourceType, "--use-connection-strings", "true"); + + Assert.Equal(HttpStatusCode.OK, optedIn.Status); + Assert.Contains("If username and password are required", optedIn.Message); + Assert.Contains("Create secrets in Key Vault to store the connection string", optedIn.Message); + } + + [Theory] + [InlineData("bicep")] + [InlineData("terraform")] + public async Task AzureSqlNetworking_RequiresExplicitPublicOptIn(string iacType) + { + var secure = await ExecuteCommandAsync("--deployment-tool", "AzCli", "--iac-type", iacType, + "--resource-types", "azuresqldatabase"); + + Assert.Equal(HttpStatusCode.OK, secure.Status); + Assert.Contains("Microsoft.Sql/servers logical server", secure.Message); + Assert.Contains("publicNetworkAccess to Disabled", secure.Message); + Assert.Contains("privatelink.database.windows.net", secure.Message); + Assert.Contains("Use Microsoft Entra managed identity", secure.Message); + Assert.DoesNotContain("{{", secure.Message); + + var optedIn = await ExecuteCommandAsync("--deployment-tool", "AzCli", "--iac-type", iacType, + "--resource-types", "azuresqldatabase", "--enable-public-network-access", "true"); + + Assert.Equal(HttpStatusCode.OK, optedIn.Status); + Assert.Contains("publicNetworkAccess to Enabled", optedIn.Message); + Assert.DoesNotContain("privatelink.database.windows.net", optedIn.Message); + Assert.DoesNotContain("0.0.0.0", optedIn.Message); + } + + [Fact] + public async Task SecurityGuidance_RejectsAzureExceptionWithoutPublicAccess() + { + var result = await ExecuteCommandAsync("--deployment-tool", "AzCli", "--allow-azure-services", "true"); + Assert.Equal(HttpStatusCode.BadRequest, result.Status); + } + [Fact] public async Task Should_get_infrastructure_code_rules() { @@ -43,13 +123,15 @@ public async Task Should_get_infrastructure_rules_for_terraform() Assert.Contains("main.tf", result.Message, StringComparison.OrdinalIgnoreCase); } - [Fact] - public async Task Should_get_infrastructure_rules_for_function_app() + [Theory] + [InlineData("bicep")] + [InlineData("terraform")] + public async Task Should_get_infrastructure_rules_for_function_app(string iacType) { // arrange & act var result = await ExecuteCommandAsync( "--deployment-tool", "azd", - "--iac-type", "bicep", + "--iac-type", iacType, "--resource-types", "function"); // assert @@ -57,7 +139,20 @@ public async Task Should_get_infrastructure_rules_for_function_app() Assert.Equal(HttpStatusCode.OK, result.Status); Assert.NotNull(result.Message); Assert.Contains("Additional requirements for Function Apps", result.Message, StringComparison.OrdinalIgnoreCase); - Assert.Contains("Storage Blob Data Owner", result.Message, StringComparison.OrdinalIgnoreCase); + Assert.DoesNotContain("Storage Blob Data Owner", result.Message, StringComparison.OrdinalIgnoreCase); + } + + [Theory] + [InlineData("bicep", "Microsoft.Authorization/roleAssignments")] + [InlineData("terraform", "azurerm_role_assignment")] + public async Task FunctionAppOwnerRole_RequiresExplicitOptIn(string iacType, string assignmentType) + { + var result = await ExecuteCommandAsync("--deployment-tool", "azd", "--iac-type", iacType, + "--resource-types", "function", "--allow-privileged-roles", "true"); + + Assert.Equal(HttpStatusCode.OK, result.Status); + Assert.Contains("Storage Blob Data Owner", result.Message); + Assert.Contains(assignmentType, result.Message); } [Fact]