feat(azure): add Deploy-to-Azure Bicep template and certificate auth

This commit is contained in:
Lydia Vilchez committed 2026-08-12 16:21:32 +02:00
1 parent 0a6206e9a8
commit 903025aadc
7 files changed
+352 -302

No files matched your search

@@ -57,13 +57,29 @@ Azure supports two authentication methods in the add-provider wizard. Prowler Cl
#### Certificate Authentication (Recommended)
The quick-start flow: click **Deploy to Azure** in the wizard to start a deployment in the Azure Portal with the Prowler Bicep template pre-loaded. The template creates the App Registration / Service Principal, the `Reader` role assignment, and the custom `ProwlerRole` bound to a certificate credential. You still fill the certificate parameter and confirm the deployment inside the Portal.
Certificate authentication uses an Entra ID App Registration with an X.509 keyCredential. The Prowler wizard splits the setup in two steps: the manual pieces that only the Azure Portal UI supports (creating the App Registration and uploading the certificate), and the RBAC pieces the Prowler Bicep template deploys with one click on Azure Portal (`Reader` + custom `ProwlerRole`).
<Note>
**Why the split?** Microsoft Graph resources in Bicep (`Microsoft.Graph/applications`, `Microsoft.Graph/servicePrincipals`) are [not supported by Portal `Deploy to Azure` URL deployments](https://github.com/microsoftgraph/msgraph-bicep-types/issues/294). A template that contains them fails at deploy time with `Authorization_RequestDenied` regardless of the deploying user's Entra ID role — even a Global Administrator hits the wall. The wizard therefore creates the App Registration through the Portal UI (which works normally) and reserves the Bicep template for the RBAC assignments (which Portal deployments handle cleanly).
</Note>
<Note>
**Required permissions in the Azure account you run the setup with**
Two separate permission systems apply:
- **Microsoft Entra ID**: `Application Administrator`, `Cloud Application Administrator`, or `Global Administrator` — needed for the manual App Registration + certificate upload in steps 1-2 below. Not required if your tenant already allows all users to register applications (Entra ID → Users → User settings).
- **Azure RBAC**: `Owner` on the target subscription — needed by the Deploy to Azure click in step 3 to assign `Reader` and the custom `ProwlerRole`.
If you do not have both, ask your Azure administrator to grant them or to complete the missing step on your behalf.
</Note>
1. In the Azure wizard, select **Certificate Authentication (Recommended)**.
2. Click the **Deploy to Azure** button. This opens the Azure Portal deployment blade pre-loaded with the [Prowler Bicep template](https://github.com/prowler-cloud/prowler/blob/master/permissions/templates/azure/bicep/prowler-scan.bicep).
3. Generate a certificate locally and paste the base64-encoded public certificate into the Bicep `certificateBase64` parameter. See [Certificate Authentication → Generating the certificate](/user-guide/providers/azure/authentication#generating-the-certificate) for the exact commands.
4. When the deployment finishes, copy the `tenantId` and `applicationId` outputs into Prowler along with the base64-encoded **PEM bundle** (certificate + private key) you generated earlier, and paste it into the Certificate Content field.
5. Click **Next**, then **Launch Scan**.
2. **Create the App Registration** — click *Open Azure Portal → New App Registration* in the wizard, sign in to the tenant you want Prowler to scan, and register an application. Any name works (e.g. `Prowler`); keep the default single-tenant audience and no redirect URI.
3. **Upload the certificate** — in the wizard, click *Generate certificate for me* to produce a keypair in your browser (the private key auto-fills the form; the public certificate downloads as a base64 text file). Decode it with `base64 -D -i prowler-cert-base64.txt -o prowler.cer`, then in the Portal open your new App Registration → *Certificates & secrets → Certificates → Upload certificate* and upload `prowler.cer`. Prefer the CLI? Follow the openssl / PowerShell steps in [Certificate Authentication → Generating the certificate](/user-guide/providers/azure/authentication#generating-the-certificate).
4. **Copy the Service Principal Object ID** — Portal → Entra ID → *Enterprise applications* → search for the app you just created → *Overview → Object ID*. Beware this is a different Object ID than the one on the *App registrations* blade (Enterprise applications and App registrations are two separate objects with separate Object IDs but the same Application (client) ID).
5. **Grant Prowler read access** — click **Deploy to Azure** in the wizard. The Azure Portal opens with the Prowler Bicep template pre-loaded. Pick your target subscription, paste the Service Principal Object ID from step 4 into the `servicePrincipalObjectId` field, and confirm the deployment.
6. **Fill the wizard** — back in Prowler, paste the `Tenant ID` from Entra ID → Overview, the `Application (client) ID` from your App Registration's Overview page, and confirm the `Certificate Private Key` field is populated from step 3. Click **Next**, then **Launch Scan**.
#### Service Principal with Client Secret
+101 -55
View File
@@ -5,30 +5,65 @@ button in the Prowler UI (`add-provider` wizard → Azure → Certificate
authentication). It is the Azure equivalent of the CloudFormation quick-create
stack under `../../cloudformation/`.
Deploying `prowler-scan.bicep` at subscription scope provisions everything
Prowler needs to authenticate against the subscription and enumerate resources:
Deploying `prowler-scan.bicep` at subscription scope grants a **pre-existing**
App Registration the read-only permissions Prowler needs:
1. An Entra ID App Registration with an X.509 `keyCredentials` entry
(certificate authentication — no client secrets).
2. A Service Principal for that application.
3. A subscription-scoped assignment of the built-in `Reader` role.
4. A subscription-scoped custom role (`ProwlerRole`) with the two extra
1. A subscription-scoped assignment of the built-in `Reader` role.
2. A subscription-scoped custom role (`ProwlerRole`) with the two extra
read/list actions the built-in Reader is missing, and its role assignment.
The template is idempotent: role definitions and role assignments use
deterministic GUIDs derived from `subscription().id` and `applicationName`,
so redeploying updates the existing resources instead of creating duplicates.
deterministic GUIDs derived from `subscription().id` and the deployment
label, so redeploying updates the existing resources instead of creating
duplicates.
## Why the template doesn't create the App Registration itself
Microsoft.Graph Bicep resources (`Microsoft.Graph/applications`,
`Microsoft.Graph/servicePrincipals`, etc.) are **not supported by the
Azure Portal "Deploy to Azure" URL flow**. This is a documented Microsoft
limitation, not a permission or configuration problem — see
[microsoftgraph/msgraph-bicep-types#294](https://github.com/microsoftgraph/msgraph-bicep-types/issues/294)
(closed as documented limitation) and Microsoft's own
[permissions and privileges docs](https://learn.microsoft.com/en-us/graph/templates/bicep/concept-permissions-and-privileges),
which list only Azure CLI and Azure PowerShell as supported deployment paths
for templates containing `Microsoft.Graph/*` resources.
A template that includes those resources fails at Portal deploy time with
`Authorization_RequestDenied: Insufficient privileges to complete the
operation` from Microsoft Graph, regardless of the deploying user's Entra
ID role — a **Global Administrator** hits the same wall. The wizard
therefore splits the flow: the user creates the App Registration and
uploads the certificate manually in the Portal (both operations *are*
supported through the Portal UI), then this template only grants that
existing service principal the RBAC roles Prowler needs.
## Required permissions to run the deployment
The account that clicks **Deploy to Azure** (or runs `az deployment sub
create` locally) needs `Owner` on the target subscription. That is enough
to create the custom role definition and assign both roles. `Contributor`
is not sufficient because it cannot create role assignments.
Creating the App Registration and uploading its certificate (the manual
Portal steps *before* the Deploy to Azure click) require either the tenant
setting *Users can register applications* to be enabled, or one of
`Application Administrator`, `Cloud Application Administrator`, or
`Global Administrator` in Entra ID. The wizard surfaces these prerequisites
on the Certificate Authentication step.
## Files
- `prowler-scan.bicep` — Bicep source (source of truth). Uses the
`Microsoft.Graph` extension registered in `bicepconfig.json`.
- `prowler-scan.bicep` — Bicep source (source of truth). Vanilla ARM only —
no `Microsoft.Graph` extension, so it deploys cleanly through the Portal.
- `prowler-scan.json` — compiled ARM JSON. **This is the artifact the
Deploy to Azure button loads** — Azure Portal's
`#create/Microsoft.Template/uri/<url>` deep link only accepts ARM JSON,
not raw Bicep source. Keep it committed and in sync with the `.bicep`.
- `bicepconfig.json` — registers the Microsoft.Graph Bicep extension against
the public Microsoft Container Registry.
- `bicepconfig.json` — kept for the `az bicep build` invocation; no
extensions are required now that Microsoft.Graph resources have moved
to the manual Portal step, but the config file lets us re-add the
extension if a future Microsoft update supports Portal deployments.
- `Makefile` — `make build` regenerates the JSON; `make check` fails when
the JSON is stale relative to the Bicep source (used by CI).
@@ -51,45 +86,58 @@ After editing `prowler-scan.bicep`, run:
make build
```
Requires the Bicep CLI **>= 0.36.1** (needed by the GA release of the
Microsoft Graph Bicep extension, `v1.0.0`). Install with `az bicep install`
or grab the standalone binary from
<https://github.com/Azure/bicep/releases>. Note: the `apiVersion: v1.0` on
`Microsoft.Graph/applications` in the .bicep source refers to the Graph API
version, not the extension release channel — the extension version is pinned
in `bicepconfig.json`.
Requires the Bicep CLI. Install with `az bicep install` or grab the
standalone binary from <https://github.com/Azure/bicep/releases>.
## Generating the certificate
## Manual App Registration + certificate steps (what the wizard guides)
The user supplies the certificate — the template only receives the
base64-encoded DER of the public certificate. Prowler never sees the private
key during deployment; Microsoft Graph computes and stores the SHA-1
thumbprint from the certificate bytes and returns it as a template output.
1. **Create the App Registration** in Portal → **Microsoft Entra ID** →
**App registrations** → **New registration**. Give it any name, keep
the default *single tenant* audience, no redirect URI.
2. **Upload the certificate** on the same App Registration → **Certificates
and secrets** → **Certificates** tab → **Upload certificate**. Upload
the `.cer` (public) file you generated. Prowler's *Generate certificate
for me* button in the wizard produces a base64 file that decodes back to
the required `.cer` (see the wizard for the exact one-liner).
3. **Copy the Service Principal Object ID**: Portal → **Microsoft Entra
ID** → **Enterprise applications** → search for the app you just
created → click it → **Object ID** on the Overview page. That is the
value the Bicep template asks for as `servicePrincipalObjectId`. It is
NOT the same as the App Registration's Object ID (Enterprise
applications and App registrations are two separate objects with
separate Object IDs — same App ID / Client ID, different Object IDs).
4. **Copy the Application (client) ID** from the App Registration overview
— you paste this in the Prowler wizard's *Client ID* field.
### macOS / Linux (OpenSSL)
### Certificate generation cheatsheet
`Generate certificate for me` in the wizard is the easiest option — it
generates a keypair in your browser, auto-fills the private key into the
wizard, and downloads a text file with the base64-encoded public
certificate you upload to the App Registration.
If you prefer the command line:
#### macOS / Linux (OpenSSL)
```bash
# 1. Generate a 4096-bit RSA private key and a matching self-signed cert
# 1. Generate a 4096-bit RSA private key and matching self-signed cert
openssl req -x509 -newkey rsa:4096 -keyout prowler.key -out prowler.crt \
-days 365 -nodes -subj "/CN=Prowler"
# 2. Base64-encode the DER form of the public certificate. Feeds
# `certificateBase64`. IMPORTANT: use `-outform DER` — piping a PEM
# file through base64 double-encodes it and the deployment will fail.
CERT_BASE64=$(openssl x509 -in prowler.crt -outform DER | base64 | tr -d '\n')
# 2. Upload prowler.crt to the App Registration (Portal, step 2 above).
# 3. Concatenate the certificate AND the private key into a single PEM
# bundle, then base64-encode the whole thing. `azure.identity.CertificateCredential`
# needs BOTH parts — a key-only file fails with "No certificate found".
# Keep this value secret. Using `< file` (not `-i`) works on macOS and Linux.
# 3. Bundle certificate + private key into a single PEM and base64-encode
# it. `azure.identity.CertificateCredential` needs BOTH parts — a
# key-only file fails with "No certificate found". Keep this value
# secret; it is what Prowler uses to authenticate.
cat prowler.crt prowler.key > prowler-bundle.pem
CERT_BUNDLE_BASE64=$(base64 < prowler-bundle.pem | tr -d '\n')
echo "certificateBase64 (paste into the Bicep parameter): $CERT_BASE64"
echo "certificate bundle (paste into Prowler UI, keep secret): $CERT_BUNDLE_BASE64"
echo "Certificate Private Key for Prowler wizard: $CERT_BUNDLE_BASE64"
```
### Windows (PowerShell)
#### Windows (PowerShell)
```powershell
$cert = New-SelfSignedCertificate -Subject "CN=Prowler" `
@@ -97,16 +145,15 @@ $cert = New-SelfSignedCertificate -Subject "CN=Prowler" `
-KeyExportPolicy Exportable -KeySpec Signature `
-KeyLength 4096 -HashAlgorithm SHA256
# certificateBase64: DER of the public certificate, base64-encoded
$certBase64 = [Convert]::ToBase64String($cert.RawData)
# Save the .cer to upload in the Portal
Export-Certificate -Cert $cert -FilePath prowler.cer
# Private key (PKCS#12), base64-encoded — paste into Prowler as
# "Certificate Content", keep secret
# Private key (PKCS#12), base64-encoded — paste into the Prowler wizard as
# Certificate Private Key. Keep secret.
$pfxBytes = $cert.Export('Pfx', '')
$keyBase64 = [Convert]::ToBase64String($pfxBytes)
Write-Host "certificateBase64 (paste into the Bicep parameter): $certBase64"
Write-Host "private key (paste into Prowler UI, keep secret): $keyBase64"
Write-Host "Certificate Private Key for Prowler wizard: $keyBase64"
```
## Deploying manually (CLI, when the button is not an option)
@@ -117,7 +164,7 @@ Deploy the compiled ARM JSON (recommended, matches what the Portal loads):
az deployment sub create \
--location westeurope \
--template-file prowler-scan.json \
--parameters certificateBase64="$CERT_BASE64"
--parameters servicePrincipalObjectId=<sp-object-id>
```
Or deploy the Bicep source directly (Azure CLI compiles it on the fly):
@@ -126,22 +173,21 @@ Or deploy the Bicep source directly (Azure CLI compiles it on the fly):
az deployment sub create \
--location westeurope \
--template-file prowler-scan.bicep \
--parameters certificateBase64="$CERT_BASE64"
--parameters servicePrincipalObjectId=<sp-object-id>
```
## After the deployment
Paste the following into the Prowler wizard's Certificate Authentication form:
- **Tenant ID** — `tenantId` output.
- **Client ID** — `applicationId` output.
- **Certificate Content** — the base64-encoded **PEM bundle** (certificate +
private key) or PKCS#12 export you generated above. The Bicep template
received only the public certificate; Prowler needs the certificate paired
with the private key to sign token requests.
The `certificateThumbprint` output is informational; use it to verify the
deployed keyCredential matches the certificate you generated locally.
- **Tenant ID** — the `tenantId` output (also visible in Portal → Entra ID
→ Overview).
- **Client ID** — the Application (client) ID of the App Registration you
created manually in step 1 of the manual flow above.
- **Certificate Private Key** — the base64-encoded PEM bundle (certificate
+ private key) or PKCS#12 export from the generation step. This is
Prowler's copy of the private half of the keypair; it never leaves the
wizard and never touches Azure.
The manual fallback described in the Azure authentication docs (client
secrets, sovereign clouds, personal accounts) remains supported for
@@ -1,49 +1,45 @@
// -----------------------------------------------------------------------------
// Prowler quick-start deployment
// Prowler quick-start deployment (RBAC only)
//
// One-shot Bicep template that stands up everything Prowler needs to scan an
// Azure subscription with certificate-based authentication:
// 1. An Entra ID App Registration with an X.509 certificate credential
// (`keyCredentials`) — no client secrets, no deploymentScripts.
// 2. A Service Principal for that App Registration.
// 3. A role assignment granting the built-in `Reader` role at subscription
// scope so Prowler can inventory resources.
// 4. A subscription-scoped custom "ProwlerRole" that grants the two extra
// Subscription-scoped template that grants a pre-existing App Registration
// / Service Principal the read-only permissions Prowler needs to scan an
// Azure subscription:
//
// 1. Assignment of the built-in `Reader` role at subscription scope so
// Prowler can inventory resources.
// 2. A subscription-scoped custom "ProwlerRole" that grants the two extra
// read/list actions the built-in Reader is missing
// (`Microsoft.Web/sites/host/listkeys/action` and
// `Microsoft.Web/sites/config/list/Action`), plus its role assignment.
//
// The template does NOT create the App Registration itself. Microsoft.Graph
// Bicep resources are not supported by the Azure Portal "Deploy to Azure"
// flow (Microsoft docs: `msgraph-bicep-types` issue #294, closed as a
// documented limitation), so any template that includes them fails at
// deploy time with `Authorization_RequestDenied` regardless of the user's
// Entra ID role. The wizard therefore guides the user to create the App
// Registration and upload the certificate manually in the Portal first,
// then deploys this template with the resulting service principal's
// Object ID.
//
// This mirrors the AWS CloudFormation quick-create flow shipped in
// `permissions/templates/cloudformation/prowler-scan-role.yml`, and matches
// the "manual" instructions in
// `docs/user-guide/providers/azure/authentication.mdx`. Keep them in sync
// when adding or removing permissions here.
//
// After the deployment succeeds, copy the outputs (tenantId, clientId, and
// certificateThumbprint) into Prowler along with the private key that pairs
// with `certificateBase64`.
// After the deployment succeeds, copy the outputs (tenantId, subscriptionId)
// into Prowler along with the App Registration's Client ID and the private
// key that pairs with the certificate uploaded to Entra ID manually.
// -----------------------------------------------------------------------------
targetScope = 'subscription'
@description('Name for the App Registration Prowler will use. Keep the default unless you need a custom name.')
param applicationName string = 'ProwlerApp'
@description('Object ID of the Service Principal for the App Registration Prowler will use. Find it in Azure Portal → Microsoft Entra ID → Enterprise applications → your app → Overview → Object ID. This is NOT the same as the App Registration\'s Object ID; the Service Principal has its own separate Object ID.')
param servicePrincipalObjectId string
@description('Paste the PUBLIC certificate from the Prowler wizard (prowler-cert-base64.txt) or your own openssl output. Prowler never receives the matching private key.')
@secure()
param certificateBase64 string
@description('Label for the certificate inside the App Registration. Cosmetic — keep the default if unsure.')
param certificateDisplayName string = 'Prowler Certificate'
@description('When the certificate becomes valid. Defaults to now.')
param certificateStartDateTime string = utcNow()
// Derived from `certificateStartDateTime` (not a fresh `utcNow()`) so the
// pair is always exactly 1 year apart — Microsoft Graph rejects certificates
// whose `endDateTime - startDateTime` exceeds the 1-year cap.
@description('When the certificate expires. Defaults to 1 year after certificateStartDateTime. Rotate before this date.')
param certificateEndDateTime string = dateTimeAdd(certificateStartDateTime, 'P1Y')
@description('Cosmetic label used inside the role assignment names so multiple deployments do not collide. Free text; keep the default unless you need to distinguish multiple Prowler deployments.')
param deploymentLabel string = 'Prowler'
@description('Name of the extra role Prowler creates. Keep the default unless your org already uses this name.')
param customRoleName string = 'ProwlerRole'
@@ -52,53 +48,12 @@ param customRoleName string = 'ProwlerRole'
// re-deploying the same template into the same subscription is idempotent —
// the role definition and the two role assignments are found and updated
// instead of duplicated. `guid()` is safe to call at subscription scope.
// The role-assignment names cannot reference `servicePrincipal.id`
// because ARM needs those names calculable before the deployment starts;
// pinning to `applicationName` gives a stable identity per app.
var customRoleDefinitionName = guid(subscription().id, customRoleName)
var readerRoleDefinitionId = subscriptionResourceId(
'Microsoft.Authorization/roleDefinitions',
'acdd72a7-3385-48ef-bd42-f606fba81ae7' // built-in Reader
)
// Microsoft Graph resources are provisioned through the `Microsoft.Graph`
// Bicep extension. `apiVersion: v1.0` is the stable channel; the resource
// names below are the type names Graph exposes for Bicep, not ARM.
extension microsoftGraphV1
// `uniqueName` on Microsoft.Graph/applications is tenant-scoped (an existing
// value UPSERTs the resource), so two subscriptions in the same tenant
// deploying with the default `applicationName` would otherwise overwrite
// each other's keyCredentials and silently break the previous scan. Suffix
// with the subscription id to keep each deployment isolated. The visible
// display name stays as the user-provided `applicationName`.
resource application 'Microsoft.Graph/applications@v1.0' = {
uniqueName: '${applicationName}-${subscription().subscriptionId}'
displayName: applicationName
description: 'Deployed by Prowler quick-start Bicep template for read-only security scanning.'
signInAudience: 'AzureADMyOrg'
keyCredentials: [
{
displayName: certificateDisplayName
type: 'AsymmetricX509Cert'
usage: 'Verify'
// Microsoft Graph computes `customKeyIdentifier` (the base64-encoded
// SHA-1 thumbprint) from the certificate bytes automatically, so we
// deliberately omit it. Passing the hex thumbprint here would fail the
// keyCredentials contract, which expects base64-encoded bytes.
key: certificateBase64
startDateTime: certificateStartDateTime
endDateTime: certificateEndDateTime
}
]
}
resource servicePrincipal 'Microsoft.Graph/servicePrincipals@v1.0' = {
appId: application.appId
displayName: applicationName
}
// Custom role: only the two read/list actions the built-in Reader is missing.
// Keep this list in sync with `permissions/prowler-azure-custom-role.json`.
resource prowlerRole 'Microsoft.Authorization/roleDefinitions@2022-05-01-preview' = {
@@ -125,18 +80,18 @@ resource prowlerRole 'Microsoft.Authorization/roleDefinitions@2022-05-01-preview
}
resource readerAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(subscription().id, applicationName, 'Reader')
name: guid(subscription().id, servicePrincipalObjectId, deploymentLabel, 'Reader')
properties: {
principalId: servicePrincipal.id
principalId: servicePrincipalObjectId
principalType: 'ServicePrincipal'
roleDefinitionId: readerRoleDefinitionId
}
}
resource prowlerRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(subscription().id, applicationName, customRoleName)
name: guid(subscription().id, servicePrincipalObjectId, deploymentLabel, customRoleName)
properties: {
principalId: servicePrincipal.id
principalId: servicePrincipalObjectId
principalType: 'ServicePrincipal'
roleDefinitionId: prowlerRole.id
}
@@ -144,11 +99,4 @@ resource prowlerRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-
output tenantId string = subscription().tenantId
output subscriptionId string = subscription().subscriptionId
output applicationId string = application.appId
output servicePrincipalObjectId string = servicePrincipal.id
// Microsoft Graph populates `customKeyIdentifier` (the base64-encoded SHA-1
// thumbprint) on the created keyCredential; expose it as an output so users
// can reconcile the deployed cert against the fingerprint they generated
// locally.
output certificateThumbprint string = application.keyCredentials[0].customKeyIdentifier
output prowlerRoleDefinitionId string = prowlerRole.id
@@ -1,47 +1,25 @@
{
"$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#",
"languageVersion": "2.0",
"contentVersion": "1.0.0.0",
"metadata": {
"_generator": {
"name": "bicep",
"version": "0.46.1.21595",
"templateHash": "10591927438861815055"
"templateHash": "17956756534127211092"
}
},
"parameters": {
"applicationName": {
"servicePrincipalObjectId": {
"type": "string",
"defaultValue": "ProwlerApp",
"metadata": {
"description": "Name for the App Registration Prowler will use. Keep the default unless you need a custom name."
"description": "Object ID of the Service Principal for the App Registration Prowler will use. Find it in Azure Portal → Microsoft Entra ID → Enterprise applications → your app → Overview → Object ID. This is NOT the same as the App Registration's Object ID; the Service Principal has its own separate Object ID."
}
},
"certificateBase64": {
"type": "securestring",
"metadata": {
"description": "Paste the PUBLIC certificate from the Prowler wizard (prowler-cert-base64.txt) or your own openssl output. Prowler never receives the matching private key."
}
},
"certificateDisplayName": {
"deploymentLabel": {
"type": "string",
"defaultValue": "Prowler Certificate",
"defaultValue": "Prowler",
"metadata": {
"description": "Label for the certificate inside the App Registration. Cosmetic — keep the default if unsure."
}
},
"certificateStartDateTime": {
"type": "string",
"defaultValue": "[utcNow()]",
"metadata": {
"description": "When the certificate becomes valid. Defaults to now."
}
},
"certificateEndDateTime": {
"type": "string",
"defaultValue": "[dateTimeAdd(parameters('certificateStartDateTime'), 'P1Y')]",
"metadata": {
"description": "When the certificate expires. Defaults to 1 year after certificateStartDateTime. Rotate before this date."
"description": "Cosmetic label used inside the role assignment names so multiple deployments do not collide. Free text; keep the default unless you need to distinguish multiple Prowler deployments."
}
},
"customRoleName": {
@@ -56,45 +34,8 @@
"customRoleDefinitionName": "[guid(subscription().id, parameters('customRoleName'))]",
"readerRoleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]"
},
"imports": {
"microsoftGraphV1": {
"provider": "MicrosoftGraph",
"version": "1.0.0"
}
},
"resources": {
"application": {
"import": "microsoftGraphV1",
"type": "Microsoft.Graph/applications@v1.0",
"properties": {
"uniqueName": "[format('{0}-{1}', parameters('applicationName'), subscription().subscriptionId)]",
"displayName": "[parameters('applicationName')]",
"description": "Deployed by Prowler quick-start Bicep template for read-only security scanning.",
"signInAudience": "AzureADMyOrg",
"keyCredentials": [
{
"displayName": "[parameters('certificateDisplayName')]",
"type": "AsymmetricX509Cert",
"usage": "Verify",
"key": "[parameters('certificateBase64')]",
"startDateTime": "[parameters('certificateStartDateTime')]",
"endDateTime": "[parameters('certificateEndDateTime')]"
}
]
}
},
"servicePrincipal": {
"import": "microsoftGraphV1",
"type": "Microsoft.Graph/servicePrincipals@v1.0",
"properties": {
"appId": "[reference('application').appId]",
"displayName": "[parameters('applicationName')]"
},
"dependsOn": [
"application"
]
},
"prowlerRole": {
"resources": [
{
"type": "Microsoft.Authorization/roleDefinitions",
"apiVersion": "2022-05-01-preview",
"name": "[variables('customRoleDefinitionName')]",
@@ -118,34 +59,30 @@
]
}
},
"readerAssignment": {
{
"type": "Microsoft.Authorization/roleAssignments",
"apiVersion": "2022-04-01",
"name": "[guid(subscription().id, parameters('applicationName'), 'Reader')]",
"name": "[guid(subscription().id, parameters('servicePrincipalObjectId'), parameters('deploymentLabel'), 'Reader')]",
"properties": {
"principalId": "[reference('servicePrincipal').id]",
"principalId": "[parameters('servicePrincipalObjectId')]",
"principalType": "ServicePrincipal",
"roleDefinitionId": "[variables('readerRoleDefinitionId')]"
},
"dependsOn": [
"servicePrincipal"
]
}
},
"prowlerRoleAssignment": {
{
"type": "Microsoft.Authorization/roleAssignments",
"apiVersion": "2022-04-01",
"name": "[guid(subscription().id, parameters('applicationName'), parameters('customRoleName'))]",
"name": "[guid(subscription().id, parameters('servicePrincipalObjectId'), parameters('deploymentLabel'), parameters('customRoleName'))]",
"properties": {
"principalId": "[reference('servicePrincipal').id]",
"principalId": "[parameters('servicePrincipalObjectId')]",
"principalType": "ServicePrincipal",
"roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('customRoleDefinitionName'))]"
},
"dependsOn": [
"prowlerRole",
"servicePrincipal"
"[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('customRoleDefinitionName'))]"
]
}
},
],
"outputs": {
"tenantId": {
"type": "string",
@@ -155,18 +92,6 @@
"type": "string",
"value": "[subscription().subscriptionId]"
},
"applicationId": {
"type": "string",
"value": "[reference('application').appId]"
},
"servicePrincipalObjectId": {
"type": "string",
"value": "[reference('servicePrincipal').id]"
},
"certificateThumbprint": {
"type": "string",
"value": "[reference('application').keyCredentials[0].customKeyIdentifier]"
},
"prowlerRoleDefinitionId": {
"type": "string",
"value": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('customRoleDefinitionName'))]"
@@ -1 +1 @@
Add a Deploy to Azure quick-start to the add-provider wizard. Azure now shows a credential-type selector (Certificate authentication recommended, Service Principal with Client Secret fallback), and the certificate form exposes a one-click button that opens the Azure Portal deployment blade pre-loaded with the Prowler Bicep template (App Registration + Service Principal + Reader + custom ProwlerRole + certificate credential)
Add a Deploy to Azure quick-start to the add-provider wizard. Azure now shows a credential-type selector (Certificate authentication recommended, Service Principal with Client Secret fallback). The certificate form guides the user through creating the App Registration and uploading the certificate in the Portal (with an in-browser "Generate certificate for me" helper that keeps the private key client-side), then a one-click Deploy to Azure button that opens the Portal deployment blade pre-loaded with the Prowler Bicep template to assign the built-in Reader role and a custom ProwlerRole to that App Registration's service principal
@@ -17,6 +17,12 @@ import { getAzureDeploymentQuickLink } from "@/lib/external-urls";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
import { AzureCertificateCredentials } from "@/types";
const AZURE_PORTAL_NEW_APP_REGISTRATION_URL =
"https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/CreateApplicationBlade";
const AZURE_PORTAL_ENTERPRISE_APPLICATIONS_URL =
"https://portal.azure.com/#view/Microsoft_AAD_IAM/StartboardApplicationsMenuBlade/~/AppAppsPreview";
export const AzureCertificateCredentialsForm = ({
control,
}: {
@@ -24,9 +30,10 @@ export const AzureCertificateCredentialsForm = ({
}) => {
// The Deploy-to-Azure link is a plain constant today: unlike the AWS
// organisation link, it does not need any wizard-collected value in the
// URL (subscription selection happens inside the Portal deployment blade).
// If Prowler ever needs to pin the deployment to a specific subscription
// or region, thread that value through `getAzureDeploymentQuickLink` here.
// URL (subscription and the service principal Object ID are filled inside
// the Portal deployment blade). Prefilling the SP Object ID via the URL
// would need the Portal's uiFormDefinitionUri machinery, which is more
// moving parts than the copy-paste we already ask the user to do.
const deployToAzureUrl = getAzureDeploymentQuickLink();
// Feedback state for the in-browser certificate generator. Kept local
@@ -54,7 +61,7 @@ export const AzureCertificateCredentialsForm = ({
{ shouldValidate: true, shouldDirty: true, shouldTouch: true },
);
// Hand the public certificate to the user as a file: they upload it to
// the Bicep `Certificate Base64` parameter in the Azure Portal.
// the App Registration's Certificates blade in the Azure Portal.
downloadPublicCertificateFile(result.publicCertificateBase64Der);
setGeneratedThumbprint(result.thumbprintHex);
} catch (error) {
@@ -75,62 +82,164 @@ export const AzureCertificateCredentialsForm = ({
Certificate Authentication (Recommended)
</div>
<div className="text-text-neutral-tertiary text-sm">
The Deploy to Azure quick-start opens the Azure Portal with the
Prowler Bicep template pre-loaded to provision the App Registration /
Service Principal and read-only roles. Paste the resulting credentials
below when the deployment finishes.
Prowler authenticates against Azure with an X.509 certificate bound
to an Entra ID App Registration. The setup is a two-step flow: create
the App Registration and upload the certificate through the Azure
Portal, then click <strong>Deploy to Azure</strong> below to grant
that App Registration the read-only permissions Prowler needs on the
subscription.
</div>
</div>
<div className="flex flex-col items-start gap-1">
<Button variant="link" size="link-sm" asChild>
<a href={deployToAzureUrl} target="_blank" rel="noopener noreferrer">
Deploy to Azure
</a>
</Button>
<p className="text-text-neutral-tertiary text-xs">
After deployment, copy <strong>Tenant ID</strong> and{" "}
<strong>Application ID</strong> from the deployment outputs. The
certificate private key you generated locally goes in the
&ldquo;Certificate Private Key&rdquo; field below.
</p>
</div>
<div className="border-content-neutral-tertiary flex flex-col items-start gap-2 rounded-md border border-dashed p-3">
<div className="border-content-warning-tertiary bg-content-warning-tertiary/10 flex flex-col gap-2 rounded-md border p-3">
<div className="text-text-neutral-primary text-sm font-semibold">
Don&apos;t have a certificate yet?
Before you start
</div>
<p className="text-text-neutral-tertiary text-xs">
Generate a self-signed X.509 certificate right in your browser.
<strong>
{" "}
The certificate and private key are generated locally
</strong>
. The base64-encoded certificate/private-key bundle goes into the
field below and is submitted with the rest of the form. The public
certificate downloads as a text file for you to paste into the Bicep{" "}
<code>Certificate Base64</code> parameter in the Portal.
Your Azure account needs permissions in <em>two</em> separate systems.
The wizard cannot work around a missing role — request them from your
Azure administrator if needed:
</p>
<Button
type="button"
variant="default"
size="sm"
onClick={handleGenerateCertificate}
disabled={isGeneratingCert}
>
{isGeneratingCert ? "Generating…" : "Generate certificate for me"}
</Button>
{generatorError && (
<p className="text-text-error-primary text-xs">{generatorError}</p>
)}
{generatedThumbprint && (
<p className="text-text-success-primary text-xs">
Done. Certificate SHA-1 thumbprint:{" "}
<code>{generatedThumbprint}</code>. Downloaded{" "}
<code>prowler-cert-base64.txt</code> — paste its contents into the
Bicep <code>Certificate Base64</code> parameter, then keep filling
the fields below.
</p>
)}
<ul className="text-text-neutral-tertiary ml-4 list-disc space-y-1 text-xs">
<li>
<strong>Microsoft Entra ID</strong>:{" "}
<strong>Application Administrator</strong>,{" "}
<strong>Cloud Application Administrator</strong>, or{" "}
<strong>Global Administrator</strong> — needed to create the App
Registration and upload the certificate in step 1. Not required if
your tenant already allows all users to register applications
(Entra ID → Users → User settings).
</li>
<li>
<strong>Azure RBAC</strong>: <strong>Owner</strong> on the target
subscription — needed by <em>Deploy to Azure</em> in step 3 to
assign the <code>Reader</code> and custom <code>ProwlerRole</code>{" "}
roles.
</li>
</ul>
</div>
<ol className="border-content-neutral-tertiary flex flex-col gap-4 rounded-md border p-4">
<li className="flex flex-col gap-2">
<div className="text-text-neutral-primary text-sm font-semibold">
1. Create the App Registration in Azure
</div>
<p className="text-text-neutral-tertiary text-xs">
Open the Portal in a new tab, sign in to the tenant you want
Prowler to scan, and register a new application. Any name works
(e.g. <code>Prowler</code>); keep the default single-tenant
audience and no redirect URI.
</p>
<Button variant="link" size="link-sm" asChild>
<a
href={AZURE_PORTAL_NEW_APP_REGISTRATION_URL}
target="_blank"
rel="noopener noreferrer"
>
Open Azure Portal → New App Registration
</a>
</Button>
</li>
<li className="flex flex-col gap-2">
<div className="text-text-neutral-primary text-sm font-semibold">
2. Attach a certificate to that App Registration
</div>
<p className="text-text-neutral-tertiary text-xs">
In your new App Registration, open{" "}
<em>Certificates &amp; secrets → Certificates → Upload
certificate</em> and upload the public certificate file
(<code>.cer</code>). The button below generates the keypair in your
browser — the private key is auto-filled into the form below and
the public certificate downloads as a text file you decode and
upload.
</p>
<div className="border-content-neutral-tertiary flex flex-col items-start gap-2 rounded-md border border-dashed p-3">
<p className="text-text-neutral-tertiary text-xs">
<strong>The certificate never leaves your browser.</strong>{" "}
Prowler generates the keypair client-side and only receives the
private half; the public half goes to Entra ID via the manual
upload.
</p>
<Button
type="button"
variant="default"
size="sm"
onClick={handleGenerateCertificate}
disabled={isGeneratingCert}
>
{isGeneratingCert ? "Generating…" : "Generate certificate for me"}
</Button>
{generatorError && (
<p className="text-text-error-primary text-xs">
{generatorError}
</p>
)}
{generatedThumbprint && (
<p className="text-text-success-primary text-xs">
Done. Certificate SHA-1 thumbprint:{" "}
<code>{generatedThumbprint}</code>. Downloaded{" "}
<code>prowler-cert-base64.txt</code> — decode it back to a{" "}
<code>.cer</code> with{" "}
<code>base64 -D -i prowler-cert-base64.txt -o prowler.cer</code>{" "}
and upload <code>prowler.cer</code> in the Portal.
</p>
)}
<p className="text-text-neutral-tertiary text-xs">
Prefer the command line? See the{" "}
<Link
href="https://docs.prowler.com/user-guide/providers/azure/authentication#certificate-authentication"
target="_blank"
rel="noopener noreferrer"
className="text-button-tertiary p-0 text-sm"
>
openssl / PowerShell instructions
</Link>
.
</p>
</div>
</li>
<li className="flex flex-col gap-2">
<div className="text-text-neutral-primary text-sm font-semibold">
3. Grant the App Registration read access to your subscription
</div>
<p className="text-text-neutral-tertiary text-xs">
You need the App Registration&apos;s <strong>Service Principal
Object ID</strong> — not the same as the App Registration&apos;s
Object ID. Find it in{" "}
<Link
href={AZURE_PORTAL_ENTERPRISE_APPLICATIONS_URL}
target="_blank"
rel="noopener noreferrer"
className="text-button-tertiary p-0 text-sm"
>
Entra ID → Enterprise applications
</Link>{" "}
→ search for the app you just created → <em>Overview → Object
ID</em>. Then click Deploy to Azure below and paste that Object ID
when the Portal asks.
</p>
<Button variant="link" size="link-sm" asChild>
<a
href={deployToAzureUrl}
target="_blank"
rel="noopener noreferrer"
>
Deploy to Azure
</a>
</Button>
</li>
<li className="flex flex-col gap-2">
<div className="text-text-neutral-primary text-sm font-semibold">
4. Come back here and fill the fields below
</div>
<p className="text-text-neutral-tertiary text-xs">
Copy the <strong>Tenant ID</strong> from Entra ID → Overview,
the <strong>Application (client) ID</strong> from your App
Registration&apos;s Overview page, and paste both into the fields
below. The private key field is already pre-filled from step 2 (or
paste your own base64-encoded PEM bundle / PKCS#12 if you
generated the keypair manually).
</p>
</li>
</ol>
<WizardInputField
control={control}
name="tenant_id"
@@ -156,17 +265,17 @@ export const AzureCertificateCredentialsForm = ({
name="certificate_content"
label="Certificate Private Key (Base64)"
labelPlacement="inside"
placeholder="Paste the base64-encoded private key that pairs with the certificate deployed to Entra ID"
placeholder="Paste the base64-encoded private key that pairs with the certificate uploaded to Entra ID"
variant="bordered"
isRequired
minRows={4}
/>
<p className="text-text-neutral-tertiary text-sm">
This is the <strong>base64-encoded private key</strong> that matches the
certificate uploaded to Entra ID by the Bicep template (not the public
This is the <strong>base64-encoded private key</strong> that matches
the certificate you uploaded to Entra ID in step 2 (not the public
certificate, and not the thumbprint). Use the{" "}
<em>Generate certificate for me</em> button above, or follow the manual
openssl / PowerShell instructions in the{" "}
<em>Generate certificate for me</em> button in step 2, or follow the
manual openssl / PowerShell instructions in the{" "}
<Link
href="https://docs.prowler.com/user-guide/providers/azure/authentication#certificate-authentication"
target="_blank"
+16 -10
View File
@@ -40,16 +40,22 @@ export const getAttackPathHubUrl = (queryId: string): string =>
export const PROWLER_CF_TEMPLATE_URL =
"https://prowler-cloud-public.s3.eu-west-1.amazonaws.com/permissions/templates/aws/cloudformation/prowler-scan-role.yml";
// ARM JSON template URL for the Prowler quick-start Azure deployment (App
// Registration + Service Principal + Reader + custom ProwlerRole + certificate
// credential). The template is authored in Bicep at
// `permissions/templates/azure/bicep/prowler-scan.bicep` and compiled to ARM
// JSON at `prowler-scan.json` — the Azure Portal `#create/Microsoft.Template`
// deep link only accepts ARM JSON, not raw Bicep source. Regenerate with
// `make -C permissions/templates/azure/bicep build` after editing the .bicep.
// URL-encoded and appended to the Portal deep link built by
// `getAzureDeploymentQuickLink` below. Points at the same public S3 bucket
// that hosts the AWS templates.
// ARM JSON template URL for the Prowler quick-start Azure deployment. The
// template only grants a pre-existing App Registration the read-only
// subscription roles Prowler needs (built-in `Reader` + custom
// `ProwlerRole`); the App Registration itself and its certificate credential
// are created through the Portal UI in earlier wizard steps, because
// `Microsoft.Graph/*` Bicep resources are not deployable via the Portal
// `Deploy to Azure` URL flow (`msgraph-bicep-types` issue #294, closed as a
// documented Microsoft limitation).
//
// Authored in Bicep at `permissions/templates/azure/bicep/prowler-scan.bicep`
// and compiled to ARM JSON at `prowler-scan.json` — the Azure Portal
// `#create/Microsoft.Template` deep link only accepts ARM JSON, not raw
// Bicep source. Regenerate with `make -C permissions/templates/azure/bicep
// build` after editing the .bicep. URL-encoded and appended to the Portal
// deep link built by `getAzureDeploymentQuickLink` below. Points at the same
// public S3 bucket that hosts the AWS templates.
export const PROWLER_AZURE_BICEP_TEMPLATE_URL =
"https://prowler-cloud-public.s3.eu-west-1.amazonaws.com/permissions/templates/azure/bicep/prowler-scan.json";