CompositionalIT / farmer

Repeatable Azure deployments with ARM templates - made easy!
https://compositionalit.github.io/farmer
MIT License
527 stars 157 forks source link

Static IP arm-template #260

Closed Thorium closed 3 years ago

Thorium commented 4 years ago

I don't know how far Farmer is with this, so I'll try to help. Ignore all you already know or do better. As far as I know, you need first a "Network Interface" as a parent node.

Then you can bind resources like a

...under that "Network Interface". But for easier management, different "Network Interface"s can share one "Network Security Group", so you don't have to configure multiple times the same firewall-settings. Let's start with the simplest:

Static IP

This is a template creating a static IP:

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "publicIPAddresses_Myserver_ip_name": {
            "defaultValue": "Myserver-ip",
            "type": "String"
        }
    },
    "variables": {},
    "resources": [
        {
            "type": "Microsoft.Network/publicIPAddresses",
            "apiVersion": "2020-04-01",
            "name": "[parameters('publicIPAddresses_Myserver_ip_name')]",
            "location": "uksouth",
            "sku": {
                "name": "Basic"
            },
            "properties": {
                "ipAddress": "123.123.123.123",
                "publicIPAddressVersion": "IPv4",
                "publicIPAllocationMethod": "Static",
                "idleTimeoutInMinutes": 4,
                "dnsSettings": {
                    "domainNameLabel": "mydomain",
                    "fqdn": "mydomain.uksouth.cloudapp.azure.com"
                },
                "ipTags": []
            }
        }
    ]
}

Network Interface

This is how to define a "Network Interface" and bind the IP to it: (The parameters/publicIPAddresses_Myserver_ip_externalid, I don't know if that could be done any easier way).

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "networkInterfaces_mynetworkinterface_name": {
            "defaultValue": "mynetworkinterface",
            "type": "String"
        },
        "publicIPAddresses_Myserver_ip_externalid": {
            "defaultValue": "/subscriptions/<subscription-Guid>/resourceGroups/MyGroup/providers/Microsoft.Network/publicIPAddresses/Myserver-ip",
            "type": "String"
        },
        "virtualNetworks_MyGroup_vnet_externalid": {
            "defaultValue": "/subscriptions/<subscription-Guid>/resourceGroups/MyGroup/providers/Microsoft.Network/virtualNetworks/MyGroup-vnet",
            "type": "String"
        },
        "networkSecurityGroups_MyNetworkSecurityGroup_externalid": {
            "defaultValue": "/subscriptions/<subscription-Guid>/resourceGroups/MyGroup/providers/Microsoft.Network/networkSecurityGroups/MyNetworkSecurityGroup",
            "type": "String"
        }
    },
    "variables": {},
    "resources": [
        {
            "type": "Microsoft.Network/networkInterfaces",
            "apiVersion": "2020-04-01",
            "name": "[parameters('networkInterfaces_mynetworkinterface_name')]",
            "location": "uksouth",
            "properties": {
                "ipConfigurations": [
                    {
                        "name": "ipconfig1",
                        "properties": {
                            "privateIPAddress": "10.0.0.8",
                            "privateIPAllocationMethod": "Dynamic",
                            "publicIPAddress": {
                                "id": "[parameters('publicIPAddresses_Myserver_ip_externalid')]"
                            },
                            "subnet": {
                                "id": "[concat(parameters('virtualNetworks_MyGroup_vnet_externalid'), '/subnets/default')]"
                            },
                            "primary": true,
                            "privateIPAddressVersion": "IPv4"
                        }
                    }
                ],
                "dnsSettings": {
                    "dnsServers": []
                },
                "enableAcceleratedNetworking": false,
                "enableIPForwarding": false,
                "networkSecurityGroup": {
                    "id": "[parameters('networkSecurityGroups_MyNetworkSecurityGroup_externalid')]"
                }
            }
        }
    ]
}

To attach this "Network Interface", it's defined in e.g. VirtualMachine parameters-section:

        "networkInterfaces_mynetworkinterface_externalid": {
            "defaultValue": "/subscriptions/<subscription-Guid>/resourceGroups/MyGroup/providers/Microsoft.Network/networkInterfaces/mynetworkinterface",
            "type": "String"
        }

and resources-section as:

                "networkProfile": {
                    "networkInterfaces": [
                        {
                            "id": "[parameters('networkInterfaces_mynetworkinterface_externalid')]"
                        }
                    ]
                },

Network Security Group

...and then the "Network Security Group", this is used in "Network Interface" above:

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "networkSecurityGroups_MyNetworkSecurityGroup_name": {
            "defaultValue": "MyNetworkSecurityGroup",
            "type": "String"
        }
    },
    "variables": {},
    "resources": [
        {
            "type": "Microsoft.Network/networkSecurityGroups",
            "apiVersion": "2020-04-01",
            "name": "[parameters('networkSecurityGroups_MyNetworkSecurityGroup_name')]",
            "location": "uksouth",
            "properties": {
                "securityRules": [
                    {
                        "name": "MyHttps",
                        "properties": {
                            "protocol": "TCP",
                            "sourcePortRange": "*",
                            "destinationPortRange": "443",
                            "sourceAddressPrefix": "*",
                            "destinationAddressPrefix": "*",
                            "access": "Allow",
                            "priority": 1000,
                            "direction": "Inbound",
                            "sourcePortRanges": [],
                            "destinationPortRanges": [],
                            "sourceAddressPrefixes": [],
                            "destinationAddressPrefixes": []
                        }
                    },
                    {
                        "name": "Some-deny-example",
                        "properties": {
                            "protocol": "*",
                            "sourcePortRange": "*",
                            "destinationPortRange": "1-50000",
                            "destinationAddressPrefix": "*",
                            "access": "Deny",
                            "priority": 999,
                            "direction": "Inbound",
                            "sourcePortRanges": [],
                            "destinationPortRanges": [],
                            "sourceAddressPrefixes": [
                                "123.90.119.1",
                                "123.29.164.2",
                                //...
                            ],
                            "destinationAddressPrefixes": []
                        }
                    }
                ]
            }
        },
        {
            "type": "Microsoft.Network/networkSecurityGroups/securityRules",
            "apiVersion": "2020-04-01",
            "name": "[concat(parameters('networkSecurityGroups_MyNetworkSecurityGroup_name'), '/MyHome')]",
            "dependsOn": [
                "[resourceId('Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroups_MyNetworkSecurityGroup_name'))]"
            ],
            "properties": {
                "protocol": "*",
                "sourcePortRange": "*",
                "sourceAddressPrefix": "123.123.74.0/24",
                "destinationAddressPrefix": "*",
                "access": "Allow",
                "priority": 2001,
                "direction": "Inbound",
                "sourcePortRanges": [],
                "destinationPortRanges": [
                    "443",
                    //...
                ],
                "sourceAddressPrefixes": [],
                "destinationAddressPrefixes": []
            }
        },
        {
            "type": "Microsoft.Network/networkSecurityGroups/securityRules",
            "apiVersion": "2020-04-01",
            "name": "[concat(parameters('networkSecurityGroups_MyNetworkSecurityGroup_name'), '/MyHome2')]",
            "dependsOn": [
                "[resourceId('Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroups_MyNetworkSecurityGroup_name'))]"
            ],
            "properties": {
                "protocol": "*",
                "sourcePortRange": "*",
                "sourceAddressPrefix": "123.123.169.0/24",
                "destinationAddressPrefix": "*",
                "access": "Allow",
                "priority": 2002,
                "direction": "Inbound",
                "sourcePortRanges": [],
                "destinationPortRanges": [
                    "3389",
                    "443",
                    //...
                ],
                "sourceAddressPrefixes": [],
                "destinationAddressPrefixes": []
            }
        }
    ]
}
isaacabraham commented 4 years ago

Looping @ninjarobot in on this one again. Networks seem to be hot at the moment :-)

ninjarobot commented 4 years ago

I agree we need some sort of an IP address builder to make it possible to associate the IP's with vnet subnets, security rules, etc - trying to do this in the context of the resource using them (a VM, a container group, etc) can make it difficult to represent them in a complex network topology.

Baseline would be to have a type-safe builder around the public IP addresses and network security groups, but it can get unwieldy as the topologies grow, so we probably don't want to stop there.

The NSG's rules themselves are a bit low level - a flat set of ordered rules about ports and addresses - so I've been prototyping with some different ideas about how to build them a little smarter. You generally will care more about what the IP address (and NIC) is attached to and the services you are run. I'm borrowing some concepts from another project I did.


module FwRules =
    /// Endpoint is an address or a subnet
    type Endpoint =
        | Host of IPAddress
        | Network of IPAddress * SubnetNumber:int

    /// Networking protocols
    type Protocol =
        | All
        | Tcp
        | Udp
        | Icmp

    /// What to do with the traffic
    type Operation =
        | Accept
        | Reject
        | Drop

    /// A port could be a single port or a range of them
    type Port =
        | Port of uint16
        | Range of Start:uint16 * End:uint16

    /// A service is a higher level representation of the endpoint(s) where an application may listen
    type Service =
        | Service of Name:string * Protocol * Port
        | Services of Name:string * Service list

/// Some example services, like http and postres
let http = 
    Services ("http",
        [
            Service("http", Tcp, Port (80us))
            Service("https", Tcp, Port (443us))
        ])
let postgres = Service ("pgsql", Tcp, Port (5432us))

/// Anywhere - usually the public internet
let anywhere = Network (IPAddress.Parse "0.0.0.0", 0)

A policy is a logical grouping of rules that can be built from the lower level services and endpoints. These give a reasonable view of the services that are communicating.


/// Allow anything communicate inbound to http services on load balancers
let lbPolicy = {
    Name = "public to load balancers"
    Service = http
    Source = [ Anywhere ]
    Destination = [ loadBalancer ]
    Operation = Accept
}
/// Allow the load balancers communicate to the http services on the web servers
let webPolicy = {
    Name = "lb to web servers"
    Service = http
    Source = [ loadBalancer ]
    Destination = webservers
    Operation = Accept
}
/// Allow only the web servers to communicate with the database servers
let dbPolicy = {
    Name = "web to database servers"
    Service = postgres
    Source = webservers
    Destination = dbservers
    Operation = Accept
}
/// Deny all other traffic
let denyAllPolicy = {
    Name = "deny all"
    Service = Service ("Anything", Tcp, Range (0us, 65535us))
    Source = []
    Destination = []
    Operation = Reject
}

With those policies defined, you could associate various resources with a policy so that as resources come and go, they can get the correct type of IP address, correct subnet they belong to, correct network security rules.

let webserver = vm {
    set_policy webPolicy
    // lots of other settings here...
}
let dbserver = vm {
    set_policy dbPolicy
    // lots of other settings here...
}

I'd like to hear your feedback @Thorium and @isaacabraham and others before I get too far into it. Too complex to set up endpoints, services, and policies or is this a useful representation to minimize what you'll need to specify on the resources themselves?

Thorium commented 4 years ago

Thank @ninjarobot, looks good! I see this as typical scenario for hosting services:

a) 2 different servers instances (VMs or whatever) and any kind of load-balancer (I use Traffic Manager) to ensure 99.99% SLA: while deploying updates to the other, the other can be running. b) For the server(s), the following (same) firewall rules:

  1. There are web-pages, and SSL-web-pages.
  2. And there is some kind of test-environment or API-development endpoint.
  3. Besides that, there can be some level management or monitoring pages: Better would be a VPN-connection, but often it's just too heavy solution when all you want to do is check a minor detail with your mobile phone.
  4. If you use Azure VMs, they have by default public RDP open. That has to be limited to at least only a certain IPs.

Now, any of the later parts, they are some non-standard ports and you might want to restrict the public access, and control the access by allowed IP-addresses only. That is kind of annoying as many users use non-static IP-addresses and IPs change when they restart a router, switch mobile phone network, restart VMs, etc. That maintenance-burden of changing-customer-IPs would be nice to minimize but I don't know how.

I don't know if Farmer already supports kind of progressive-deployment "only the recent changes please, not the whole thing again", something like that would really help here.

For a rule, I like the idea of name, but also a date would be nice: "This IP was added to the firewall around 5 years ago, maybe it's not needed anymore". And possibly a kind of import-from-a-table/csv, to migrate existing 100+ rules with less manual work.

ninjarobot commented 4 years ago

I added #281 that can create NSG templates with security rules. It's a WIP since I'm still working on the builders, but @Thorium and @isaacabraham please review the implementation and model so I can incorporate your feedback.

ninjarobot commented 4 years ago

@Thorium since NSG rules have a description, that seemed like a good place to put the date, if you want it. Here is an example in the tests.

only the recent changes please

ARM deployments generally do this, although I think this is really specific to the subresource's internal implementation. Either way, they should be idempotent from an ARM API standpoint.

ninjarobot commented 4 years ago

@Thorium please take look at #281 - this gives some robust NSG support.

Basic scenario with an HTTPS web service:

let httpsAccess = securityRule {
    name "web-servers"
    service (Service ("https", Port 443us))
    add_source_tag TCP "Internet"
    add_destination_any
}
let myNsg = nsg {
    name "my-nsg"
    add_rules [
        httpsAccess
    ]
}

You often have much more complex ones and want to expose the same sets of services in different ways, so there are a few concepts.

I suggest using the description on the securityRule to give it a date or any other information you want. It will remain present on the rule in the NSG. The NSG security rules are additive, so to add more rules later, you can specify only the additional rules in another deployment - existing ones that aren't present in the deployment are left in place.

Please give it a try. As always, feedback is welcome.

Thorium commented 3 years ago

I think all this is doable with 1.6.8