Post

Private Endpoint can be a threat

In cloud environments, private endpoints are often used to connect to services securely and privately. While this is a powerful feature, like any tool, it can become a threat if you don’t fully understand how it works. In one of my previous posts, I demonstrated how you can mitigate these threats using Azure Policy. In this post, I’d like to combine both topics, as they are equally important.

Microsoft picture

What is a Private Endpoint?

A Private Endpoint is a network interface that connects you privately and securely to a service powered by Azure Private Link. It uses a private IP address from your VNet, effectively bringing the service into your VNet. The service can be an Azure service like Azure Storage or Azure SQL, your service deployed in Azure, or even a service hosted in your on-premises network.

Why is it a threat?

When we create a Private Endpoint, it uses the Azure Backbone network to connect to the service, and this connection is not restricted to your tenant or subscription. This means that, with the right permissions, the service can be accessed from any subscription or tenant. If an insider with access becomes malicious, or if credentials are compromised, private endpoints can be created to access your services without your knowledge.

How can we protect from this threat?

Azure Policy provides a straightforward way to mitigate this threat. By creating a policy that denies the creation of private endpoints, you can prevent unauthorized access. Although this is a simple solution, it can save you significant headaches. Unfortunately, there are two ways to use private endpoints to connect to services, so these scenarios must be addressed individually.

Egress private endpoint

This is the most common use case for private endpoints: connecting to services privately. If you have a service you wish to access securely, you can create a private endpoint to connect to it.

The threat here is that the private endpoint does not need to exist in your subscription or tenant. With the appropriate permissions, you can connect to the service from any subscription or tenant. This means that even with a free tenant and subscription, an internal private endpoint could be connected to an external Azure resource, bypassing all security controls.

To mitigate this, you can create a policy that denies the creation of private endpoints.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
"if": {
 "allOf": [
  {
   "field": "type",
   "equals": "Microsoft.Network/privateEndpoints"
  },
  {
   "anyOf": [
    {
     "count": {
      "field": "Microsoft.Network/privateEndpoints/manualprivateLinkServiceConnections[*]",
      "where": {
       "allOf": [
        {
         "field": "Microsoft.Network/privateEndpoints/manualprivateLinkServiceConnections[*].privateLinkServiceId",
         "notEquals": ""
        },
        {
         "value": "[split(concat(first(field('Microsoft.Network/privateEndpoints/manualprivateLinkServiceConnections[*].privateLinkServiceId')), '//'), '/')[2]]",
         "notEquals": "[subscription().subscriptionId]"
        }
       ]
      }
     },
     "greaterOrEquals": 1
    },
    {
     "count": {
      "field": "Microsoft.Network/privateEndpoints/privateLinkServiceConnections[*]",
      "where": {
       "allOf": [
        {
         "field": "Microsoft.Network/privateEndpoints/privateLinkServiceConnections[*].privateLinkServiceId",
         "notEquals": ""
        },
        {
         "value": "[split(concat(first(field('Microsoft.Network/privateEndpoints/privateLinkServiceConnections[*].privateLinkServiceId')), '//'), '/')[2]]",
         "notEquals": "[subscription().subscriptionId]"
        }
       ]
      }
     },
     "greaterOrEquals": 1
    }
   ]
  }
 ]
},
"then": {
    "effect": "Deny"
}

Ingress private endpoint

The other approach is to create a private endpoint in the external tenant and connect it to your service. While this is more complicated, it is still possible. This poses an even bigger problem because you cannot see the private endpoint in your subscription. As a result, you cannot detect if someone has created a private endpoint to your service just by reviewing the resource configuration. Unfortunately, this means you need to create a policy for every resource, as these settings are resource-specific. Here is an example for a storage account:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
"if": {
    "allOf": [
        {
            "field": "type",
            "equals": "Microsoft.Storage/storageAccounts/privateEndpointConnections"
        },
        {
            "field": "Microsoft.Storage/storageAccounts/privateEndpointConnections/privateLinkServiceConnectionState.status",
            "equals": "Approved"
        },
        {
            "anyOf": [
                {
                    "field": "Microsoft.Storage/storageAccounts/privateEndpointConnections/privateEndpoint.id",
                    "exists": false
                },
                {
                    "value": "[split(concat(field('Microsoft.Storage/storageAccounts/privateEndpointConnections/privateEndpoint.id'), '//'), '/')[2]]",
                    "notEquals": "[subscription().subscriptionId]"
                }
            ]
        }
    ]
},
"then": {
    "effect": "Deny"
}

Conclusion

When you start using something new, always check the security settings. This doesn’t just mean reviewing the default settings but also deeply understanding how the service works and how it could potentially be exploited for malicious purposes.

This post is licensed under CC BY 4.0 by the author.