Thursday, December 23, 2010

Programmatically add/remove IP Security Restriction with C# ASP.NET


IIS7 has introduced new add element of IPSecurity collection that allows to block IP addresses directly from the web.config without changing the metadatabase. This, combined with configSource feature allows us to dump all IPs into a single xml file making it easy to manipulate and maintain. This article shows how to block IP addresses programmatically using the IPSecurity element and splitting the config file with configSource.

Basic steps:

  1. allow overrideMode for the application configuration.
  2. create xml file to contain restricted IPs
  3. change configSource in web.config to read IPSecurity info from the xml file.
  4. create application that will add/remove IPs from xml file.


Detailed guide:

  1. Make sure Role Service for IP security is already installed. To install role service see the guide at the end of the article.
  2. create web application from a specific folder in IIS7 Manager using the "Convert to Application" option.
    In this example we will create Test1 application.
  3. set overrideMode to "Allow" in the applicationHost.config for the specific website (c:\Windows\system32\inetsrv\config\applicationHost.config)


    <configuration>
    ...


    <location path="PC/Test1" overrideMode="Allow">
    <
    system.webServer>
    <security >
    <
    ipSecurity>
    </ipSecurity>
    </
    security>
    </
    system.webServer>
    </
    location>
    </configuration>




  4. create ipSecurity.xml file that will contain IP restrictions


    <?xml version="1.0" encoding="utf-8"?>
    <
    ipSecurity allowUnlisted="true">
    <
    add ipAddress="1.1.1.1" subnetMask="" allowed="true" domainName="" />
    </
    ipSecurity>



  5. change web.config to read IPSecurity section from the ipSecurity.xml file


    <?
    xml version="1.0" encoding="UTF-8"?>
    <
    configuration>
    <
    appSettings />
    <
    connectionStrings />
    <
    system.web>
    <
    authentication mode="Windows" />
    </
    system.web>
    <system.webServer>
    <
    security>
    <
    ipSecurity configSource="ipSecurity.xml" />
    </
    security>
    </
    system.webServer>
    </
    configuration>

  6. copy provided files to create admin site to add/remove IP Security restrictions for the test1 site. (default.aspx; default.aspx.cs; impersonateUser.cs;
    web.config; ).

    1. change web.config parameters


default.aspx


<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="default.aspx.cs" Inherits="IPSecurityIIS7.admin._default" %>


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">


<html xmlns="http://www.w3.org/1999/xhtml" >


<head runat="server">


<title></title>


<style type="text/css">


body


{


font-family:Tahoma,Verdana,Helvetica,Arial,sans-serif;


font-size:81.3%;


}


p {font-size:x-small;}


.data-table


{


border-bottom:1px solid #E4E4E4;


border-top:1px solid #E4E4E4;


color:#5B5B5B;


font-family:Tahoma,Verdana,Helvetica,Arial,sans-serif;


font-size:81.3%;


}


.data-table th


{


padding:5px;


background-color:#F5F5F5;


border-left:1px solid #E4E4E4;


border-right:1px solid #E4E4E4;


font-weight:bold;


padding:5px;


}


</style>


</head>


<body>


<form id="form1" runat="server">


<div>



<br />



</div>


<h2 style="text-align:center">IP Security</h2><hr />


<table style="width:100%;">


<tr>


<td>


Filter results:


<asp:RadioButton ID="rbAll" runat="server" GroupName="Allowed"


AutoPostBack="True" Checked="True" Text="All"


oncheckedchanged="rbAll_CheckedChanged" />


<asp:RadioButton ID="rbGrand" runat="server" GroupName="Allowed"


AutoPostBack="True" oncheckedchanged="rbGrand_CheckedChanged"


Text="Grand" />


<asp:RadioButton ID="rbDenied" runat="server" GroupName="Allowed"


AutoPostBack="true" oncheckedchanged="rbDenied_CheckedChanged"


Text="Denied" />


</td>


<td>


<h3 style="padding:0px;">IP Restrictions


</h3>


</td>


</tr>


<tr>


<td>


<asp:GridView ID="gvIPs" runat="server"


AutoGenerateColumns="False"


AutoGenerateDeleteButton="True"


AutoGenerateEditButton="True"


onrowediting="gvIPs_RowEditing"


onrowcancelingedit="gvIPs_RowCancelingEdit"


onselectedindexchanging="gvIPs_SelectedIndexChanging"


onrowupdating="gvIPs_RowUpdating"


onrowdeleting="gvIPs_RowDeleting"


DataKeyNames="ipAddress" AllowPaging="True"


onpageindexchanging="gvIPs_PageIndexChanging" PageSize="20"


>


<Columns>


<asp:BoundField DataField="ipAddress" HeaderText="ipAddress" ReadOnly="false" />


<asp:BoundField DataField="subnetMask" HeaderText="subnetMask" />


<asp:CheckBoxField DataField="allowed" HeaderText="allowed" />


<asp:BoundField DataField="domainName" HeaderText="domainName" />


</Columns>


</asp:GridView>


<asp:LinkButton ID="lnkBtnAdd" runat="server" onclick="lnkBtnAdd_Click">+ Add</asp:LinkButton>


<br />


<br />


<p>


<b>Note:</b>&nbsp;IP Address must be valid IP otherwise the whole site will throw an error (invalid config data)


</p>


</td>


<td valign="top">


<asp:CheckBox ID="chkAllowUnlisted" runat="server" Text="Allow Unlisted" AutoPostBack="true"


oncheckedchanged="chkAllowUnlisted_CheckedChanged" />


<br />


<br />


<asp:Button ID="btnCommitToIPSecurity" runat="server" onclick="btnResetAppPool_Click"


Text="Commit IP Security"


ToolTip="Commits right the way the changes of the config file, otherwise the changes will not be commited untill the next time the app gets recycled" />


<br />


<br />


<br />


<asp:Literal ID="ltLoginStatus" runat="server" ></asp:Literal>


</td>


</tr>


</table>


<br />


<hr style="padding:0px;" />


<br />


<br />


<div>


<h3>How to use</h3><hr />


<p>


<b>Example:</b><br />


set AllowUnlisted to false;


add new IP record with IP 22.12.123.22, subnetMask=&quot;255.255.0.0&quot; and check Allowed.



This will allow access only for IP range between 22.12.0.0 and 255.255.0.0.


</p>


</div>


<table cellspacing="0" cellpadding="0" class="data-table">


<tbody>


<tr>


<th>Attribute</th>


<th>Description</th></tr>


<tr>


<th><code>allowed</code></th>


<td>Optional Boolean attribute.<br><br>Specifies whether to allow access to the address space.<br><br>The default value is <code>false</code>.</td></tr>


<tr>


<th><code>domainName</code></th>


<td>Optional string attribute.<br><br>Specifies domain name on which to impose a restriction rule. You can use an asterisk (*) as a wildcard.</td></tr>


<tr>


<th><code>ipAddress</code></th>


<td>Optional string attribute.<br><br>Specifies the IP version 4 address on which to impose a restriction rule.</td></tr>


<tr>


<th><code>subnetMask</code></th>


<td>Optional string attribute.<br><br>Specifies the subnet mask with which to evaluate the IP address for this restriction rule. You can use a subnet mask to identify a range of IP addresses in an address space. The default value requires a direct match of the IP address being evaluated (effectively, a range of a single address).<br><br>The default value is <code>255.255.255.255</code>.</td></tr></tbody></table>


<P>Ref:


<a href="http://www.iis.net/ConfigReference/system.webServer/security/ipSecurity/add">


http://www.iis.net/ConfigReference/system.webServer/security/ipSecurity/add</a><br />


</P>


</form>


</body>


</html>



default.aspx.cs


using
System;
using
System.Collections.Generic;
using
System.Linq;
using
System.Data.Linq;
using
System.Web;
using
System.Web.UI;
using
System.Web.UI.WebControls;
using
System.Data;
using
System.Xml.Linq;
using
System.DirectoryServices;
using
Microsoft.Web.Administration;
using
System.Web.Security;
using
System.Security.Principal;
using
System.Collections.Specialized;
namespace
IPSecurityIIS7.admin
{
public partial class _default : System.Web.UI.Page
{

#region
Properties
public string ipSecurityFileName
{
get
{
return System.Configuration.ConfigurationManager.AppSettings.Get("ipSecurityFileName").ToString();
//return @"C:\Projects\TheTaxClub\WebApps\IPSecurityIIS7\admin\ipSecurity.xml";
}
}
public string appPoolName
{
get
{
return System.Configuration.ConfigurationManager.AppSettings.Get("AppPoolName").ToString();
}
}
/// <summary>
/// to encrypt/decript SecureAppSettings run one of the following cmd
/// path=%path%;C:\Windows\Microsoft.NET\Framework64\v2.0.50727
/// aspnet_regiis -pef "secureAppSettings" . -prov "DataProtectionConfigurationProvider"
/// aspnet_regiis -pdf "secureAppSettings" .
/// </summary>
public string userName
{
get
{

NameValueCollection secureAppSettings = System.Configuration.ConfigurationManager.GetSection("secureAppSettings") as NameValueCollection;
return (secureAppSettings != null) ? secureAppSettings["Username"] : "";
}
}
public string password
{
get
{
NameValueCollection secureAppSettings = System.Configuration.ConfigurationManager.GetSection("secureAppSettings") as NameValueCollection;
return (secureAppSettings != null) ? secureAppSettings["Password"] : "";
}
}
public string domain
{
get
{
NameValueCollection secureAppSettings = System.Configuration.ConfigurationManager.GetSection("secureAppSettings") as NameValueCollection;
return (secureAppSettings != null) ? secureAppSettings["Domain"] : "";
}
}

#endregion

#region
Events handlers of user actions
protected void Page_Load(object sender, EventArgs e)
{
if (!this.IsPostBack)
{
LoadData();
}
}
protected void lnkBtnAdd_Click(object sender, EventArgs e)
{
saveIP(
"1.1.1.1", "1.1.1.1", "", "false", ""); // create new record
LoadData();
}
protected void rbDenied_CheckedChanged(object sender, EventArgs e)
{
LoadData();
}
protected void rbGrand_CheckedChanged(object sender, EventArgs e)
{
LoadData();
}
protected void rbAll_CheckedChanged(object sender, EventArgs e)
{
LoadData();
}
protected void lnkBtnLogOut_Click(object sender, EventArgs e)
{
//Session.Abandon();
FormsAuthentication.SignOut();
Response.Redirect(
"../default.aspx");
}
protected void chkAllowUnlisted_CheckedChanged(object sender, EventArgs e)
{
updateIPSecurityAttributes();
}
protected void btnResetAppPool_Click(object sender, EventArgs e)
{
using (ServerManager serverManager = new ServerManager())
{
using (ImpersonateUser iu = new ImpersonateUser())
{
// impersonate user
iu.impersonateValidUser(userName, domain, password);
// recycle the pool
serverManager.ApplicationPools[appPoolName].Recycle();
// undo the impersonation
iu.undoImpersonation();
}
}
}

#endregion

#region
IP Security Data Grid
protected void gvIPs_SelectedIndexChanging(object sender, GridViewSelectEventArgs e)
{
gvIPs.SelectedIndex = e.NewSelectedIndex;
LoadData();
}
protected void gvIPs_RowEditing(object sender, GridViewEditEventArgs e)
{
gvIPs.EditIndex = e.NewEditIndex;
LoadData();
}
protected void gvIPs_RowCancelingEdit(object sender, GridViewCancelEditEventArgs e)
{
gvIPs.SelectedIndex = -1;
gvIPs.EditIndex = -1;
LoadData();
}
protected void gvIPs_RowUpdating(object sender, GridViewUpdateEventArgs e)
{
// read IP from the grid
GridViewRow row = gvIPs.Rows[e.RowIndex];
string oldIP = gvIPs.DataKeys[e.RowIndex].Value.ToString();
string ip = ((TextBox)(row.Cells[1].Controls[0])).Text;
string ipMask = ((TextBox)(row.Cells[2].Controls[0])).Text;
string allowed = ((CheckBox)(row.Cells[3].Controls[0])).Checked.ToString().ToLower();
string domainName = ((TextBox)(row.Cells[4].Controls[0])).Text;
// update row
saveIP(oldIP, ip, ipMask, allowed, domainName);
// cancel edit
gvIPs.EditIndex = -1;
// save
LoadData();
}
protected void gvIPs_RowDeleting(object sender, GridViewDeleteEventArgs e)
{
string ip = gvIPs.DataKeys[e.RowIndex].Value.ToString();
deleteIP(ip);
LoadData();
}
protected void gvIPs_PageIndexChanging(object sender, GridViewPageEventArgs e)
{
gvIPs.PageIndex = e.NewPageIndex;
LoadData();
}
private void LoadData()
{
bool showAll = rbAll.Checked;
string allowedValue = rbDenied.Checked.ToString().ToLower();
XDocument xml = LoadXML();
if (xml != null)
{
var t = from c in xml.Element("ipSecurity").Elements("add")
where showAll || c.Attribute("allowed").Value == allowedValue
select new
{
id = c.Attribute(
"ipAddress").Value,
ipAddress = (
string)c.Attribute("ipAddress") ?? "",
subnetMask = (
string)c.Attribute("subnetMask") ?? "",
allowed = (
string)c.Attribute("allowed") ?? "false",
domainName = (
string)c.Attribute("domainName") ?? ""
};
gvIPs.DataSource = t.ToList();
gvIPs.DataBind();
// get "AllowUnlisted"
var l = (from c in xml.Elements("ipSecurity")
select new { allowUnlisted = c.Attribute("allowUnlisted").Value }).FirstOrDefault();
bool allowUnlisted = false;
bool.TryParse(l.allowUnlisted, out allowUnlisted);
chkAllowUnlisted.Checked = allowUnlisted;
}
else
{
Response.Write(
@"<span style='color:darkRed;'>Security file not found!</span>");
}
}
/// <summary>
/// Read XML file
/// </summary>
/// <returns></returns>
private XDocument LoadXML()
{
if (!string.IsNullOrEmpty(ipSecurityFileName) && System.IO.File.Exists(ipSecurityFileName))
{
return XDocument.Load(ipSecurityFileName);
}
else
{
return null;
}
}
/// <summary>
/// Create/Update IP element.
/// when oldIP is found, existing IP element will be updated.
/// when oldIP is not found, new IP element will be created.
/// </summary>
/// <param name="oldIP"></param>
/// <param name="ip"></param>
/// <param name="ipMask"></param>
/// <param name="allowed"></param>
/// <param name="domainName"></param>
protected void saveIP(string oldIP, string ip, string ipMask, string allowed, string domainName)
{
XDocument d = LoadXML();
if (d != null)
{
// find existing element
var item = (from p in d.Element("ipSecurity").Elements("add")
where p.Attribute("ipAddress").Value == oldIP
select p).FirstOrDefault();
if (item == null)
{
// create
item =
new XElement("add");
IEnumerable<XElement> ips = d.Element("ipSecurity").Elements("add");
ips.Last().AddAfterSelf(item);
}
// save
item.SetAttributeValue(
"ipAddress", ip);
item.SetAttributeValue(
"subnetMask", ipMask);
item.SetAttributeValue(
"allowed", allowed);
item.SetAttributeValue(
"domainName", domainName);
d.Save(ipSecurityFileName);
}
}
/// <summary>
/// Delete IP from the list
/// </summary>
/// <param name="ip"></param>
protected void deleteIP(string ip)
{
XDocument d = LoadXML();
if (d != null)
{
var item = (from p in d.Element("ipSecurity").Elements("add")
where p.Attribute("ipAddress").Value == ip
select p).FirstOrDefault();
if (item != null)
{
item.Remove();
d.Save(ipSecurityFileName);
}
}
}
/// <summary>
/// Update AllowUnlisted attribute
/// </summary>
private void updateIPSecurityAttributes()
{
XDocument d = LoadXML();
if (d != null)
{
// find existing element
var item = (from p in d.Elements("ipSecurity")
select p).FirstOrDefault();
if (item != null)
{
item.SetAttributeValue(
"allowUnlisted", chkAllowUnlisted.Checked.ToString().ToLower());
d.Save(ipSecurityFileName);
}
}
}

#endregion

}
}



Any change of the xml file are not active until the application pool is recycled. Following is the code that recycles the application pool.

protected void btnResetAppPool_Click(object sender, EventArgs e)
{
using (ServerManager serverManager = new ServerManager())
{
using (ImpersonateUser iu = new ImpersonateUser())
{
// impersonate user
iu.impersonateValidUser(userName, domain, password);
// recycle the pool
serverManager.ApplicationPools[appPoolName].Recycle();
// undo the impersonation
iu.undoImpersonation();
}
}
}



by default the asp user doesn't have sufficient access rights to recycle the application pool that's why the recycle method is nested inside impersonation context.


ImpersonateUser.cs



using
System;

using
System.Web;

using
System.Web.Security;

using
System.Security.Principal;

using
System.Runtime.InteropServices;

public
class ImpersonateUser:IDisposable

{

public const int LOGON32_LOGON_INTERACTIVE = 2;

public const int LOGON32_PROVIDER_DEFAULT = 0;

WindowsImpersonationContext impersonationContext;

[
DllImport("advapi32.dll")]

public static extern int LogonUserA(string lpszUserName,

string lpszDomain,

string lpszPassword,

int dwLogonType,

int dwLogonProvider,

ref IntPtr phToken);

[
DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]

public static extern int DuplicateToken(IntPtr hToken,

int impersonationLevel,

ref IntPtr hNewToken);

[
DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]

public static extern bool RevertToSelf();

[
DllImport("kernel32.dll", CharSet = CharSet.Auto)]

public static extern bool CloseHandle(IntPtr handle);

public bool impersonateValidUser(String userName, String domain, String password)

{

WindowsIdentity tempWindowsIdentity;

IntPtr token = IntPtr.Zero;

IntPtr tokenDuplicate = IntPtr.Zero;

if (RevertToSelf())

{

if (LogonUserA(userName, domain, password, LOGON32_LOGON_INTERACTIVE,

LOGON32_PROVIDER_DEFAULT,
ref token) != 0)

{

if (DuplicateToken(token, 2, ref tokenDuplicate) != 0)

{

tempWindowsIdentity =
new WindowsIdentity(tokenDuplicate);

impersonationContext = tempWindowsIdentity.Impersonate();

if (impersonationContext != null)

{

CloseHandle(token);

CloseHandle(tokenDuplicate);

return true;

}

}

}

}

if (token != IntPtr.Zero)

CloseHandle(token);

if (tokenDuplicate != IntPtr.Zero)

CloseHandle(tokenDuplicate);

return false;

}

public void undoImpersonation()

{

impersonationContext.Undo();

}

 


#region
IDisposable Members

public void Dispose()

{

// nothing to dispose

}


#endregion


}


web.config


<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<
configSections>


<section name="secureAppSettings" type="System.Configuration.NameValueSectionHandler,System, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
</
configSections>


<appSettings>
<
add key="ipSecurityFileName" value="C:\APPS\Test1\ipSecurity.xml"></add>
<
add key="AppPoolName" value="TEst"></add>
</
appSettings>


<secureAppSettings>
<
add key="Username" value=""></add>
<
add key="Password" value=""></add>
<
add key="Domain" value=""></add>
</
secureAppSettings>


<connectionStrings />


</configuration>




To protect the impersonation information run the following command to encrypt the <secureAppSettings section

aspnet_regiis -pef "secureAppSettings" . -prov "DataProtectionConfigurationProvider"

(if aspnet_regiis is not found try to add a path to it e.g. path=%path%;C:\Windows\Microsoft.NET\Framework64\v2.0.50727)




Installing the role service for IP security

Windows Server 2008 or Windows Server 2008 R2


  1. On the taskbar, click Start, point to Administrative Tools, and then click Server Manager.
  2. In the Server Manager hierarchy pane, expand Roles, and then click Web Server (IIS).
  3. In the Web Server (IIS) pane, scroll to the Role Services section, and then click Add Role Services.
  4. On the Select Role Services page of the Add Role Services Wizard, select IP and Domain Restrictions, and then click Next.

  5. On the Confirm Installation Selections page, click Install.
  6. On the Results page, click Close.

Windows Vista or Windows 7


  1. On the taskbar, click Start, and then click Control Panel.
  2. In Control Panel, click Programs and Features, and then click Turn Windows Features on or off.
  3. Expand Internet Information Services, then World Wide Web Services, then Security.
  4. Select IP Security, and then click OK.


     

Manual restriction of IP

For manual restriction of IP addresses in IIS7 Manager follow this steps.

  1. open IIS Manager.
  2. In the Connections pane, expand the server name, expand Sites, and then site, application or Web service for which you want to add IP restrictions.
  3. In the home pane you will see the icon for "IPv4 Address and Domain Restrictions". Use this feature to allow/deny IP access to the website.




1 comment:

Luke said...

The problem with this solution is that by recycling the app pool you wipe any sessions currently in use in any of the applications running in the pool.

This is no good, we need to be able to update this list while keeping user sessions.