En el proyecto actual hemos tenido la necesidad de limitar el acceso los Web Roles de manera que sólo determinadas IPs pueden alcanzar el sitio web público. Básicamente se trata de limitar la superficie de exposición de la aplicación ya que el objetivo de la solución desarrollada no es de ámbito global si no local (se podría decir que es una aplicación de Intranet).

Si se busca en la web sobre “direcciones IPs, restricción y Azure”, puede ser que se llegue a encontrar algo sobre “NetworkTrafficRules” que en realidad no es lo que estamos buscando. Esta característica sirve para identificar qué roles pueden comunicarse con determinados “End Points”, esto es, se limita al tráfico interno dentro de los data centers de Azure.

Lo que buscamos está relacionado con la característica de IIS de restricción de IPs y Dominios.

image

Para resolver este problema, hemos utilizado la siguiente aproximación:

  1. Habilitar la restricción de IPs y dominios de IIS en los Web Roles.
  2. Definir, en el servicio (.csdef), un nuevo elemento que contenga la lista de IPs admitidas.
  3. Aplicar las restricciones cuando el Web Role arranca o cuando hay cambios en la configuración.

Para ello, hemos creado un script que habilita en el IIS del Web Role la característica de restricción de IPs y dominios (que, por defecto, en las máquinas de Azure no está habilitada):

@echo off
@echo Installing “IP Address and Domain Restrictions” feature
%windir%\System32\ServerManagerCmd.exe -install Web-IP-Security
@echo Unlocking configuration for “IP Address and Domain Restrictions” feature
%windir%\system32\inetsrv\AppCmd.exe unlock config -section:system.webServer/security/ipSecurity

Luego, utilizamos el script referenciándolo en una “Startup Task” (tarea que se ejecuta al iniciar el Role en Azure).

<WebRole name="RoleName" vmsize="Medium">
  <Startup>
    <Task executionContext="elevated" commandLine="startup/enableIpRestrictions.cmd" />
  </Startup>

Después definimos un nuevo “setting” en la definición del servicio (.csdef):

image

Con, por ejemplo, el siguiente valor asociado:

<Setting name="ipRestrictions" value="127.0.0.1,,true;192.168.100.0,255.255.255.0,true;192.205.205.1,,true" />
 

El elemento de configuración acepta valores que describen una IP concreta o un rango de IPs y si está permitida o no. Por defecto, toda comunicación que provenga de una IP que esté definida, es rechaza (a menos que el valor esté vacío). El formato que se acepta se podría definir como:

<IP><Subnet Mask><true|false>[,<IP><Subnet Mask><true|false>[,…]]

A continuación, viene la parte más compleja: el método que modifica la configuración de IIS en base a los valores establecidos en ipRestrictions. Este método se basa en el uso de la clase ServerManager. Le hemos llamado “EnableIPRestrictions()”:

public static void EnableIpRestrictions()
{
    string msg =  "Habilitando restricciones por IP.";
    EventLog.WriteEntry(EVENT_LOG_SOURCE, msg, EventLogEntryType.Information, 2000);
    Trace.TraceInformation(msg);
    try
    {
        using (ServerManager serverManager = new ServerManager())
        {
            Microsoft.Web.Administration.Configuration config = serverManager.GetApplicationHostConfiguration();
            ConfigurationSection ipSecuritySection = config.GetSection("system.webServer/security/ipSecurity");
            ipSecuritySection["allowUnlisted"] = false;
 
            ConfigurationElementCollection ipSecurityCollection = ipSecuritySection.GetCollection();
            ConfigurationElement ipClearElement = ipSecurityCollection.CreateElement("clear");
            ipSecurityCollection.Add(ipClearElement);
 
            string ipRestrictionsConfigValue = RoleEnvironment.GetConfigurationSettingValue("ipRestrictions");
            if (!String.IsNullOrWhiteSpace(ipRestrictionsConfigValue))
            {
                Trace.TraceInformation(String.Format("Valor de configuración del servicio para \"ipRestrictions\": {0}", ipRestrictionsConfigValue));
                IPRestrictionList ipList = new IPRestrictionList(ipRestrictionsConfigValue);
                foreach (IPRestriction ipDefinition in ipList)
                {
                    Trace.TraceInformation(String.Format("Añadiendo restricción: {0}", ipDefinition.ToString()));
                    ConfigurationElement ipSecurityElement = ipSecurityCollection.CreateElement("add");
                    ipSecurityElement["ipAddress"] = ipDefinition.IP.ToString();
                    if (ipDefinition.Mask != null)
                    {
                        ipSecurityElement["subnetMask"] = ipDefinition.Mask.ToString();
                    }
                    ipSecurityElement["allowed"] = ipDefinition.Allowed.ToString();
 
                    ipSecurityCollection.Add(ipSecurityElement);
                    Trace.TraceInformation(String.Format("Restricción añadida a la colección: {0}", ipDefinition.ToString()));
                }
            }
            else
            {
                ipSecuritySection["allowUnlisted"] = true;
                Trace.TraceInformation("No se han definido restricciones de IP.");
            }
 
            serverManager.CommitChanges();
            Trace.TraceInformation(String.Format("Cambios de configuración guardados correctamente"));
 
            msg = "Restricciones (si las había) aplicadas correctamente.";
            EventLog.WriteEntry(EVENT_LOG_SOURCE, msg, EventLogEntryType.Information, 2005);
            Trace.TraceInformation(msg);
        }
 
    }
    catch (Exception ex)
    {
        string message = "Se produjo un error habilitando las restricciones por IP. Excepción: " + ex.ToString();
        EventLog.WriteEntry(EVENT_LOG_SOURCE, message, EventLogEntryType.Error, 2100);
        Trace.TraceError(message);
        throw;
    }
}
Para facilitar el uso, lectura y parseo de las IPs desde la configuración, hemos incluido una clase llamada IPRestriction y una lista basada en ella:
public class IPRestrictionList : List<IPRestriction>
{
    public IPRestrictionList(string serviceConfigValue)
    {
        string[] ipEntries = serviceConfigValue.Split(';');
        if (ipEntries.Count() > 0)
        {
            foreach (string ipConfigValue in ipEntries)
            {
                if (!String.IsNullOrWhiteSpace(ipConfigValue))
                {
                    IPRestriction ip = new IPRestriction(ipConfigValue);
                    this.Add(ip);
                }
            }
        }
    }
 
}
 
public class IPRestriction
{
    public IPRestriction(string configValue)
    {
        string[] data = configValue.Split(',');
        if (data.Count() != 3) 
        {
            throw new ArgumentException("El valor {0} para crear una restricción de IP no es válido. Utilice el formato \"<Base IP>,[Mascara Subred],<true|false>\". Para definir más de una restricción, sepárelas por \";\". Por ejemplo: 192.168.1.1,,false;124.16.20.0,255.255.255.0,true", configValue);
        }
        this.IP = IPAddress.Parse(data[0]);
        this.Mask = !String.IsNullOrEmpty(data[1].Trim()) ? IPAddress.Parse(data[1]) : null;
        this.Allowed = Boolean.Parse(data[2]);
    }
 
    public IPAddress IP { get; set; }
    public IPAddress Mask { get; set; }
    public bool Allowed { get; set; }
 
    public override string ToString()
    {
        return "ipAddress=\"" + this.IP.ToString() +
               "\" " + (this.Mask != null ? "subnetMask=\"" + this.Mask.ToString() + "\" " : String.Empty) + 
               "allowed=\"" + this.Allowed.ToString() + "\"";
    }

Para finalizar, desde una clase que herede de RoleEntryPoint deberemos invocar nuestro método “EnableIPRestrictions” al iniciar el Role y cuando haya un cambio de configuración:

public class WebRole : RoleEntryPoint
{
    public override bool OnStart()
    {
        RoleEnvironment.Changed +=new EventHandler<RoleEnvironmentChangedEventArgs>(RoleEnvironmentChangedEventHandler);
        EnableIpRestrictions();
        return base.OnStart();
    }
 
    static private void RoleEnvironmentChangedEventHandler(object sender, RoleEnvironmentChangedEventArgs e)
    {
        EnableIpRestrictions();
    }
}

Con esta aproximación podemos cambiar las direcciones desde las cuales nuestra aplicación es accesible sin necesidad de redesplegar un paquete.