To download contents from a function app or app services plan, there are various APIs with associated authentication/authorization requirements.
Endpoint: The web hosting plan might have an FTPS endpoint (FTP with TLS encryption). The FTPS service is not running on the SCM/Kudu endpoint, but on the web hosting plan's underlying compute stamp, with a hostname like waws-prod-dbx-xxx.ftp.azurewebsites.windows.net
.
Credential: This endpoint might be accessible using the 'application scope' credential. We ignore the 'user scope' credential, because this is not usable for service principals, as it's tied to a human user. The 'application scope' credential is a username/password combination, whereas the username is consists of the web app's / function app's site name (mydemosite\$mydemosite
), and the password is server-generated, i.e. it can only be read (by callers who have the "Microsoft.Web/sites/config/list/Action"
permission).
Enterprises might disable these credentials (to avoid the proliferation of username/passwords). Whether usage of the credential is allowed or not can be checked by querying the /subscriptions/.../resourceGroups/.../providers/Microsoft.Web/sites/.../basicPublishingCredentialsPolicies/ftp?api-version=2022-09-01
ARM API (assuming the caller has the Microsoft.Web/sites/basicPublishingCredentialsPolicies/read
permission). The same API allows to enable/disable the credential (for callers who have the Microsoft.Web/sites/basicPublishingCredentialsPolicies/write
permission).
The SCM/Kudu site might expose a virtual file system (VFS) REST API:
This REST API is available on the URLhttps://mydemosite.scm.azurewebsites.net/api/vfs/
(the mydemosite
must of course point to your site), and can be called using 2 authentication methods:
- Application Scope Credential
- Azure AD Authentication / Bearer token
As described in the kudu docs, the "Zip" REST API allows downloading folders as zip files:
ClientSecretCredential cred = new(tenantId: ..., clientId: ..., clientSecret: ...);
AccessToken accessToken = await cred.GetTokenAsync(new(scopes: new[] { "https://management.azure.com/.default" }));
var sitename = "...";
string folderToDownload = "";
var httpClient = accessToken.CreateSCMBearerHttpClient();
using var zipStream = await httpClient.GetStreamAsync($"https://{sitename}.scm.azurewebsites.net/api/zip/{folderToDownload}");
using var fileStream = File.OpenWrite("1.zip");
await zipStream.CopyToAsync(fileStream);
The FunctionController
also allows to download a ZIP file for an Azure function. It supports the query parameters includeCsproj=true
and includeAppSettings=true
to include Visual Studio-specific files.
Your web app or function app's underlying site has 'application scope' credentials, which can be viewed in the portal under "Deployment Center -> FTPS credentials -> Application scope":
To be authorized to fetch the password, the caller must have the "Microsoft.Web/sites/config/list/Action"
permission.
The credential can also programmatically be fetched from the ARM REST API by POSTing to one of these two endpoints (depending on whether you're using a slot), of course filling in the ...
placeholders appropriately:
/subscriptions/.../resourceGroups/.../providers/Microsoft.Web/sites/.../config/publishingcredentials/list?api-version=2022-09-01
/subscriptions/.../resourceGroups/.../providers/Microsoft.Web/sites/.../slots/.../config/publishingcredentials/list?api-version=2022-09-01
This application-scoped credential represents a username/password combination, which is somewhat unrelated to Azure Resource Manager's identity system, and many organizations want to avoid having passwords like this one being used for production purposes. In particular, this warning shows that the password isn't usable for FTPS or calling Kudu APIs:
Azure ARM allows to individually allow/disallow the use of that password credential for ftp
and the scm
(Kudu) APIs.
By calling the az
CLI, you can selectively enable (allow) or disallow the use of the password (for ftp
and/or scm
):
az resource update \
--subscription "${subscriptionId}" \
--resource-group "${resourceGroupName}" \
--namespace Microsoft.Web \
--parent "sites/${siteName}" \
--resource-type basicPublishingCredentialsPolicies \
--name scm \
--set properties.allow=true
You can programmatically read/write these settings by calling the ARM REST API via the endpoint /subscriptions/.../resourceGroups/.../providers/Microsoft.Web/sites/.../basicPublishingCredentialsPolicies/scm?api-version=2022-09-01
.
Reading or writing to that endpoint requires the ARM permissions
Microsoft.Web/sites/basicPublishingCredentialsPolicies/read
Microsoft.Web/sites/basicPublishingCredentialsPolicies/write
With a comparably small set of permissions, one can retrieve the application scope credential (config/list/Action
), and if needed temporarily enable (basicPublishingCredentialsPolicies/scm/Write
) that credential:
{
"Name": "ISV Web Site Scanning Role",
"Description": "Allows download of Web App credentials and enabling and disabling basic auth for the SCM site.",
"AssignableScopes": [ "/subscriptions/${subscriptionId}" ],
"Actions": [
"Microsoft.Web/sites/config/list/Action",
"Microsoft.Web/sites/basicPublishingCredentialsPolicies/scm/Read",
"Microsoft.Web/sites/basicPublishingCredentialsPolicies/scm/Write",
"Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies/scm/Write",
"Microsoft.Web/sites/slots/basicPublishingCredentialsPolicies/scm/Read"
]
}
Installing this role, and authorizing the service principal to have that permission at subscription or management group level, allows traversing / accessing function apps and web apps. In this screenshot, you can see the app service's "Access Control (IAM)" role assignments, and that the multi-tenant app from the ISV tenant has permissions from subscription scope trickling into the app service:
The APIs on the Kudu/SCM endpoint accept AAD-issued JWT credentials, communicated in the Authorization: Bearer ...
HTTP header. The token must have an audience pointing to the Azure Management API, and the principal must have Contributor
permissions on the web app.
In comparison to the the app scope credential for basic auth, the AAD approach with Contributor permissions is pretty serious, as it gives the service principal "Actions": [ "*" ]
on the web app or even broader.
The approaches here work across AAD-tenant boundaries. If an ISV creates a multi-tenant app, and a customer installs the app in the customer's AAD tenant, the customer can grant the ISV either Contributor
permissions (for AAD-based Kudu/SCM access), or the customer creates a custom role with limited permissions to fetch/enable/disable the application-scope credentials.
One hacky way to download the contents of an app service would be this:
- Determine whether the basic publishing credentials can be used on the SCM endpoint (by fetching
.../basicPublishingCredentialsPolicies/scm
). - If the setting is
{"allow": false}
, flip it totrue
temporarily, i.e. remember to turn it off again! - Fetch the application-scope username/password by POSTing to
.../config/publishingcredentials/list
- Use that credential to recursively traverse the virtual file system endpoint
https://XXX.scm.azurewebsites.net/api/vfs/
. During the traversal, you encounter a few types of notes, depending on theirMIME
property:- The
"inode/directory"
nodes are subdirectories to traverse into. - The
"inode/shortcut"
nodes are symbolic links, leading you to the operating system files and other potentially irrelevant parts, so you might skip thesi: - All other MIME types you certainly want to download.
- The
- Once the traversal is finished, if you previously enabled the
basicPublishingCredentialsPolicies/scm
allow-setting, remember to turn it off again.
The included C# demo uses a service principal credential of a multi-tenant app to traverse
Running the C# utility against a new function app in a different tenant gives this: