Introduction

IIS 403 Forbidden access denied errors occur when the web server refuses to fulfill a valid HTTP request due to permission restrictions, authentication failures, or security policies. Unlike 401 Unauthorized (authentication required), 403 Forbidden means the server understood the request but will not authorize it regardless of authentication. Common causes include NTFS file system permissions blocking IIS_IUSRS or application pool identity, IIS authorization rules denying access, request filtering blocking file extensions or URL patterns, anonymous authentication disabled or misconfigured, application pool identity lacking required permissions, IP address restrictions blocking client, SSL/TLS required but request is HTTP, directory browsing disabled for folder without default document, handler mappings not configured for file type, and Web.config denying access to specific users or roles. The fix requires understanding IIS authentication pipeline, NTFS vs IIS permissions interaction, request filtering rules, application pool identities, and diagnostic tools. This guide provides production-proven troubleshooting for IIS 403 errors across IIS 7.5, 8.0, 8.5, and 10.0 on Windows Server 2012 R2, 2016, 2019, and 2022.

Symptoms

  • Browser displays "403 - Forbidden: Access is denied"
  • "You do not have permission to view this directory or page"
  • 403.4 - SSL Required error
  • 403.7 - Client Certificate Required error
  • 403.14 - Directory listing denied
  • 403.19 - Cannot run CGI applications
  • 403.502 - Too many requests from same IP
  • IIS logs show 403 status code with sub-status
  • Specific files accessible but others return 403
  • Website works locally but 403 from remote clients

Common Causes

  • NTFS permissions deny IIS_IUSRS or app pool identity
  • IIS authorization rules deny anonymous users
  • Request filtering blocks file extension (.dll, .exe, .config)
  • Anonymous authentication credentials misconfigured
  • Application pool identity lacks file system access
  • IP restrictions blocking client subnet
  • SSL required but accessing via HTTP
  • Default document not configured or missing
  • Handler mappings not configured for file type
  • Web.config <authorization> denying access

Step-by-Step Fix

### 1. Diagnose 403 sub-status code

Check IIS logs for sub-status:

```powershell # IIS log location # %SystemDrive%\inetpub\logs\LogFiles\W3SVC1\u_exYYMMDD.log

# Find 403 entries Select-String -Path "C:\inetpub\logs\LogFiles\W3SVC1\u_ex*.log" -Pattern "403" | Select-Object -First 20

# IIS log format (fields): # date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) sc-status sc-substatus sc-win32-status

# Example: 2024-01-15 10:30:00 192.168.1.1 GET /api/data - 80 - 10.0.0.50 Mozilla/5.0 403 14 0

# Sub-status codes: # 403.1 - Execute access forbidden # 403.2 - Read access forbidden # 403.3 - Write access forbidden # 403.4 - SSL required # 403.5 - SSL 128 required # 403.6 - IP address rejected # 403.7 - Client certificate required # 403.14 - Directory listing denied # 403.19 - CGI applications cannot run # 403.502 - Too many requests (dynamic IP restriction) ```

Enable Failed Request Tracing:

```powershell # Enable Failed Request Tracing module Import-Module WebAdministration

# Enable tracing for 403 status codes New-WebTracingRule -RequestPath "*" -StatusCode "403" -Provider "WWW Server" -Verbosity "Verbose"

# Trace files location: %SystemDrive%\inetpub\logs\FailedReqLogFiles\

# View trace in browser # http://localhost/trace.axd

# Or analyze XML files Get-ChildItem "C:\inetpub\logs\FailedReqLogFiles\W3SVC1" -Filter "*.xml" | Select-Object -First 1 | ForEach-Object { [xml](Get-Content $_.FullName) } | Select-Object -ExpandProperty failedRequest | Select-Object -ExpandProperty request

# Look for: # - MODULE_SET_RESPONSE_ERROR_STATUS # - AuthenticationModule # - RequestHandler ```

Check IIS configuration:

```powershell # Export IIS configuration %windir%\system32\inetsrv\appcmd.exe list config /xml > "$env:TEMP\iis-config.xml"

# Check specific site configuration %windir%\system32\inetsrv\appcmd.exe list config "Default Web Site" /section:system.webServer/security/authentication/anonymousAuthentication

# Check authorization rules %windir%\system32\inetsrv\appcmd.exe list config "Default Web Site" /section:system.webServer/security/authorization ```

### 2. Fix NTFS permissions

Check current permissions:

```powershell # Check folder permissions $folder = "C:\inetpub\wwwroot\yoursite" $acl = Get-Acl -Path $folder $acl.Access | Format-Table IdentityReference, FileSystemRights, AccessControlType

# Check IIS_IUSRS permission $acl.Access | Where-Object { $_.IdentityReference -like "*IIS_IUSRS*" -or $_.IdentityReference -like "*IUSR*" }

# Check application pool identity permission $appPoolName = "DefaultAppPool" $appPool = Get-IISAppPool $appPoolName $identity = $appPool.processModel.identityType

# ApplicationPoolIdentity shows as IIS APPPOOL\<poolname> ```

Grant required permissions:

```powershell # Grant IIS_IUSRS read access $acl = Get-Acl -Path $folder $rule = New-Object System.Security.AccessControl.FileSystemAccessRule( "IIS_IUSRS", "ReadAndExecute, Read, ListDirectory", "ContainerInherit, ObjectInherit", "None", "Allow" ) $acl.AddAccessRule($rule) Set-Acl -Path $folder -AclObject $acl

# Or via icacls icacls $folder /grant IIS_IUSRS:(OI)(CI)RX /T

# Grant IUSR (anonymous user) read access icacls $folder /grant IUSR:(OI)(CI)RX /T

# Grant ApplicationPoolIdentity $appPoolIdentity = "IIS APPPOOL\$appPoolName" icacls $folder /grant "$appPoolIdentity:(OI)(CI)RX" /T ```

Application pool identity configuration:

```powershell # Check app pool identity Get-IISAppPool | Select-Object Name, @{N='Identity';E={$_.processModel.identityType}}

# Change app pool identity Set-ItemProperty -Path "IIS:\AppPools\DefaultAppPool" ` -Name processModel.identityType -Value LocalService

# Identity types: # - ApplicationPoolIdentity (default, most secure) # - LocalService # - NetworkService # - LocalSystem (not recommended) # - SpecificUser (custom account)

# For custom identity Set-ItemProperty -Path "IIS:\AppPools\DefaultAppPool" -Name processModel.identityType -Value SpecificUser Set-ItemProperty -Path "IIS:\AppPools\DefaultAppPool" -Name processModel.userName -Value "DOMAIN\ServiceAccount" # Password stored encrypted in configuration ```

### 3. Configure IIS authentication

Check authentication settings:

```powershell # View authentication modules Get-WebConfigurationProperty -Filter "system.webServer/security/authentication/*" -PSPath "IIS:\Sites\Default Web Site" -Name enabled

# Check specific authentication Get-WebConfigurationProperty -Filter "system.webServer/security/authentication/anonymousAuthentication" -PSPath "IIS:\Sites\Default Web Site" -Name enabled Get-WebConfigurationProperty -Filter "system.webServer/security/authentication/windowsAuthentication" -PSPath "IIS:\Sites\Default Web Site" -Name enabled Get-WebConfigurationProperty -Filter "system.webServer/security/authentication/basicAuthentication" -PSPath "IIS:\Sites\Default Web Site" -Name enabled

# Check anonymous authentication user Get-WebConfigurationProperty -Filter "system.webServer/security/authentication/anonymousAuthentication" -PSPath "IIS:\Sites\Default Web Site" -Name userName ```

Enable anonymous authentication:

```powershell # Enable anonymous authentication Set-WebConfigurationProperty -Filter "system.webServer/security/authentication/anonymousAuthentication" -PSPath "IIS:\Sites\Default Web Site" -Name enabled -Value $true

# Set specific user for anonymous authentication Set-WebConfigurationProperty -Filter "system.webServer/security/authentication/anonymousAuthentication" -PSPath "IIS:\Sites\Default Web Site" -Name userName -Value "IUSR"

# Or use application pool identity Set-WebConfigurationProperty -Filter "system.webServer/security/authentication/anonymousAuthentication" -PSPath "IIS:\Sites\Default Web Site" -Name userName -Value "" Set-WebConfigurationProperty -Filter "system.webServer/security/authentication/anonymousAuthentication" -PSPath "IIS:\Sites\Default Web Site" -Name useAppPoolCredentials -Value $true ```

Configure Windows authentication (intranet):

```powershell # Enable Windows authentication Enable-WindowsOptionalFeature -Online -FeatureName IIS-WindowsAuthentication

# Or via IIS Manager Set-WebConfigurationProperty -Filter "system.webServer/security/authentication/windowsAuthentication" -PSPath "IIS:\Sites\Default Web Site" -Name enabled -Value $true

# Disable anonymous (require authentication) Set-WebConfigurationProperty -Filter "system.webServer/security/authentication/anonymousAuthentication" -PSPath "IIS:\Sites\Default Web Site" -Name enabled -Value $false ```

### 4. Configure IIS authorization rules

Check authorization rules:

```powershell # View authorization rules Get-WebConfigurationProperty -Filter "system.webServer/security/authorization" -PSPath "IIS:\Sites\Default Web Site"

# Output shows: # accessType: Allow/Deny # users: * or specific users # roles: specific roles # verbs: HTTP methods

# Check for deny rules blocking access Get-WebConfigurationProperty -Filter "system.webServer/security/authorization" -PSPath "IIS:\Sites\Default Web Site" | Where-Object { $_.accessType -eq "Deny" } ```

Add allow rules:

```powershell # Allow all users (anonymous access) Add-WebConfigurationProperty -Filter "system.webServer/security/authorization" -PSPath "IIS:\Sites\Default Web Site" -Name collection -Value @{accessType="Allow"; users="*"}

# Allow specific users Add-WebConfigurationProperty -Filter "system.webServer/security/authorization" -PSPath "IIS:\Sites\Default Web Site" -Name collection -Value @{accessType="Allow"; users="DOMAIN\user1,DOMAIN\user2"}

# Allow specific roles Add-WebConfigurationProperty -Filter "system.webServer/security/authorization" -PSPath "IIS:\Sites\Default Web Site" -Name collection -Value @{accessType="Allow"; roles="Administrators"}

# Remove deny rule Remove-WebConfigurationProperty -Filter "system.webServer/security/authorization" -PSPath "IIS:\Sites\Default Web Site" -Name collection -At @{accessType="Deny"; users="?"} ```

Web.config authorization:

```xml <!-- Allow all --> <configuration> <system.webServer> <security> <authorization> <add accessType="Allow" users="*" /> </authorization> </security> </system.webServer> </configuration>

<!-- Deny anonymous, allow authenticated --> <configuration> <system.webServer> <security> <authorization> <remove users="?" /> <add accessType="Allow" users="*" roles="" /> </authorization> </security> </system.webServer> </configuration>

<!-- Deny specific user --> <configuration> <system.webServer> <security> <authorization> <add accessType="Deny" users="baduser" /> <add accessType="Allow" users="*" /> </authorization> </security> </system.webServer> </configuration> ```

### 5. Configure request filtering

Check request filtering rules:

```powershell # View file extension restrictions Get-WebConfigurationProperty -Filter "system.webServer/security/requestFiltering/fileExtensions" -PSPath "IIS:\Sites\Default Web Site"

# Check hidden segments Get-WebConfigurationProperty -Filter "system.webServer/security/requestFiltering/hiddenSegments" -PSPath "IIS:\Sites\Default Web Site"

# View URL deny rules Get-WebConfigurationProperty -Filter "system.webServer/security/requestFiltering/denyUrlSequences" -PSPath "IIS:\Sites\Default Web Site" ```

Allow blocked file extensions:

```powershell # Allow specific file extension Add-WebConfigurationProperty -Filter "system.webServer/security/requestFiltering/fileExtensions" -PSPath "IIS:\Sites\Default Web Site" -Name collection -Value @{fileExtension=".extension"; allowed=$true}

# Remove file extension restriction Remove-WebConfigurationProperty -Filter "system.webServer/security/requestFiltering/fileExtensions" -PSPath "IIS:\Sites\Default Web Site" -Name collection -At @{fileExtension=".dll"}

# Allow double file extension (e.g., .tar.gz) Set-WebConfigurationProperty -Filter "system.webServer/security/requestFiltering" -PSPath "IIS:\Sites\Default Web Site" -Name allowDoubleEscaping -Value $true ```

Configure hidden segments:

```powershell # View default hidden segments (app_code, app_data, web.config, etc.) Get-WebConfigurationProperty -Filter "system.webServer/security/requestFiltering/hiddenSegments" -PSPath "IIS:\Sites\Default Web Site"

# Allow specific segment Remove-WebConfigurationProperty -Filter "system.webServer/security/requestFiltering/hiddenSegments" -PSPath "IIS:\Sites\Default Web Site" -Name collection -At @{segment="filename"}

# Common hidden segments: # - app_code # - app_data # - app_globalresources # - bin # - web.config # - .git # - .svn ```

### 6. Fix SSL/TLS requirements

Check SSL requirements:

```powershell # Check SSL settings Get-WebConfigurationProperty -Filter "system.webServer/security/access" -PSPath "IIS:\Sites\Default Web Site" -Name sslFlags

# sslFlags values: # - 0 = No SSL required # - 1 = SSL required # - 2 = SSL 128-bit required # - 4 = Client certificate required # - 8 = Client certificate accepted (optional)

# Check if SSL is required $sslFlags = Get-WebConfigurationProperty -Filter "system.webServer/security/access" -PSPath "IIS:\Sites\Default Web Site" -Name sslFlags if ($sslFlags -band 1) { Write-Host "SSL is required - accessing via HTTP will cause 403.4" } ```

Configure SSL settings:

```powershell # Disable SSL requirement (allow HTTP) Set-WebConfigurationProperty -Filter "system.webServer/security/access" -PSPath "IIS:\Sites\Default Web Site" -Name sslFlags -Value 0

# Or enable SSL (require HTTPS) Set-WebConfigurationProperty -Filter "system.webServer/security/access" -PSPath "IIS:\Sites\Default Web Site" -Name sslFlags -Value 1

# Configure via IIS Manager # Site > SSL Settings > Check/Uncheck "Require SSL"

# Bind SSL certificate New-WebBinding -Name "Default Web Site" -Protocol https -Port 443 -SslFlags 1 Set-WebConfigurationProperty -Filter "system.applicationHost/sites/site[@name='Default Web Site']/bindings/binding[@bindingInformation='*:443:']" -Name certificateHash -Value "THUMBPRINT" ```

Redirect HTTP to HTTPS:

xml <!-- Web.config URL Rewrite redirect --> <configuration> <system.webServer> <rewrite> <rules> <rule name="Redirect to HTTPS" stopProcessing="true"> <match url="(.*)" /> <conditions> <add input="{HTTPS}" pattern="off" /> </conditions> <action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" /> </rule> </rules> </rewrite> </system.webServer> </configuration>

### 7. Configure IP restrictions

Check IP restrictions:

```powershell # View IP restrictions Get-WebConfigurationProperty -Filter "system.webServer/security/ipSecurity" -PSPath "IIS:\Sites\Default Web Site"

# Check if IP restriction is enabled $ipSecurity = Get-WebConfigurationProperty -Filter "system.webServer/security/ipSecurity" -PSPath "IIS:\Sites\Default Web Site" $ipSecurity | Format-List allowUnlisted, *@name

# allowUnlisted = true means only listed IPs are denied # allowUnlisted = false means only listed IPs are allowed ```

Configure IP allow/deny:

```powershell # Allow specific IP Add-WebConfigurationProperty -Filter "system.webServer/security/ipSecurity" -PSPath "IIS:\Sites\Default Web Site" -Name collection -Value @{ipAddress="192.168.1.100"; allowed="true"}

# Deny specific IP Add-WebConfigurationProperty -Filter "system.webServer/security/ipSecurity" -PSPath "IIS:\Sites\Default Web Site" -Name collection -Value @{ipAddress="10.0.0.50"; allowed="false"}

# Allow subnet Add-WebConfigurationProperty -Filter "system.webServer/security/ipSecurity" -PSPath "IIS:\Sites\Default Web Site" -Name collection -Value @{ipAddress="192.168.1.0"; subnetMask="255.255.255.0"; allowed="true"}

# Deny all except allowed (whitelist mode) Set-WebConfigurationProperty -Filter "system.webServer/security/ipSecurity" -PSPath "IIS:\Sites\Default Web Site" -Name allowUnlisted -Value $false

# Allow all except denied (blacklist mode) Set-WebConfigurationProperty -Filter "system.webServer/security/ipSecurity" -PSPath "IIS:\Sites\Default Web Site" -Name allowUnlisted -Value $true ```

Dynamic IP restrictions:

```powershell # Enable dynamic IP restrictions (throttling) Set-WebConfigurationProperty -Filter "system.webServer/security/ipSecurity/dynamicIpSecurity" -PSPath "IIS:\Sites\Default Web Site" -Name denyByConcurrentRequests -Value $true Set-WebConfigurationProperty -Filter "system.webServer/security/ipSecurity/dynamicIpSecurity" -PSPath "IIS:\Sites\Default Web Site" -Name concurrentRequestLimit -Value 20

# Deny by request rate Set-WebConfigurationProperty -Filter "system.webServer/security/ipSecurity/dynamicIpSecurity/rateLimit" -PSPath "IIS:\Sites\Default Web Site" -Name enabled -Value $true Set-WebConfigurationProperty -Filter "system.webServer/security/ipSecurity/dynamicIpSecurity/rateLimit" -PSPath "IIS:\Sites\Default Web Site" -Name denialAction -Value "NotFound" ```

### 8. Configure default document

Check default document settings:

```powershell # View default document list Get-WebConfigurationProperty -Filter "system.webServer/defaultDocument" -PSPath "IIS:\Sites\Default Web Site" -Name files

# Check if default document is enabled Get-WebConfigurationProperty -Filter "system.webServer/defaultDocument" -PSPath "IIS:\Sites\Default Web Site" -Name enabled

# Output: 403.14 if no default document and directory browsing disabled ```

Configure default document:

```powershell # Enable default document Set-WebConfigurationProperty -Filter "system.webServer/defaultDocument" -PSPath "IIS:\Sites\Default Web Site" -Name enabled -Value $true

# Add default document Add-WebConfigurationProperty -Filter "system.webServer/defaultDocument/files" -PSPath "IIS:\Sites\Default Web Site" -Name collection -Value @{value="index.php"}

# Reorder default documents (priority) Clear-WebConfigurationProperty -Filter "system.webServer/defaultDocument/files" -PSPath "IIS:\Sites\Default Web Site" -Name collection Add-WebConfigurationProperty -Filter "system.webServer/defaultDocument/files" -PSPath "IIS:\Sites\Default Web Site" -Name collection -Value @{value="default.asp"} Add-WebConfigurationProperty -Filter "system.webServer/defaultDocument/files" -PSPath "IIS:\Sites\Default Web Site" -Name collection -Value @{value="index.htm"} Add-WebConfigurationProperty -Filter "system.webServer/defaultDocument/files" -PSPath "IIS:\Sites\Default Web Site" -Name collection -Value @{value="index.html"} Add-WebConfigurationProperty -Filter "system.webServer/defaultDocument/files" -PSPath "IIS:\Sites\Default Web Site" -Name collection -Value @{value="default.aspx"} Add-WebConfigurationProperty -Filter "system.webServer/defaultDocument/files" -PSPath "IIS:\Sites\Default Web Site" -Name collection -Value @{value="index.php"} ```

Enable directory browsing (development only):

```powershell # Enable directory browsing (not recommended for production) Set-WebConfigurationProperty -Filter "system.webServer/directoryBrowse" -PSPath "IIS:\Sites\Default Web Site" -Name enabled -Value $true

# Or via Web.config <configuration> <system.webServer> <directoryBrowse enabled="true" /> </system.webServer> </configuration> ```

### 9. Configure handler mappings

Check handler mappings:

```powershell # View handler mappings Get-WebConfigurationProperty -Filter "system.webServer/handlers" -PSPath "IIS:\Sites\Default Web Site" -Name collection

# Check specific handler Get-WebConfigurationProperty -Filter "system.webServer/handlers/add[@name='PHP_via_FastCGI']" -PSPath "IIS:\Sites\Default Web Site"

# Check if handler is allowed at site level Get-WebConfigurationProperty -Filter "system.webServer/handlers" -PSPath "IIS:\Sites\Default Web Site" -Name accessPolicy ```

Add handler mapping:

```powershell # Add PHP handler Add-WebConfigurationProperty -Filter "system.webServer/handlers" -PSPath "IIS:\Sites\Default Web Site" -Name collection -Value @{ name="PHP_via_FastCGI" path="*.php" verb="GET,HEAD,POST" modules="FastCgiModule" scriptProcessor="C:\PHP\php-cgi.exe" resourceType="Either" }

# Add custom handler Add-WebConfigurationProperty -Filter "system.webServer/handlers" -PSPath "IIS:\Sites\Default Web Site" -Name collection -Value @{ name="CustomHandler" path="*.custom" verb="GET,POST" type="Namespace.ClassName, Assembly" resourceType="Either" } ```

### 10. Debug with IIS Debug Diagnostic Tool

Download and run Debug Diagnostic Tool:

```powershell # Download Debug Diagnostic Tool v2 Update 3 # https://www.microsoft.com/en-us/download/details.aspx?id=58210

# Install and run from command line & "C:\Program Files\IIS\Debug Diagnostics Tool 2\DebugDiag.Collector.exe" -c "IIS Hangs and Crashes"

# Or create custom rule for 403 errors # 1. Open DebugDiag Collection # 2. Add Rule > Advanced Analysis # 3. Select IIS module # 4. Configure breakpoints for 403 responses ```

Use Failed Request Tracing module:

```powershell # Enable for specific site Enable-WebTracing -Site "Default Web Site" -StatusCode "403"

# View traces Get-ChildItem "C:\inetpub\logs\FailedReqLogFiles\W3SVC1" -Filter "*.xml" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 | ForEach-Object { Start-Process $_.FullName }

# Analyze trace: # Look for events with: # - MODULE_SET_RESPONSE_ERROR_STATUS # - Status: 403 # - Reason: Access denied by configuration ```

Prevention

  • Document IIS permission requirements for each application
  • Use ApplicationPoolIdentity with minimum required NTFS permissions
  • Test permission changes in development before production
  • Enable Failed Request Tracing for 403 errors in development
  • Use IIS Manager delegation for non-admin permission management
  • Regularly audit IIS configuration and NTFS permissions
  • Monitor IIS logs for 403 patterns indicating misconfiguration
  • Use Web.config transforms for environment-specific permissions
  • **403.4 - SSL Required**: Accessing HTTPS-only site via HTTP
  • **403.14 - Directory listing denied**: No default document configured
  • **403.19 - CGI cannot run**: Handler mapping or execute permissions issue
  • **403.502 - Too many requests**: Dynamic IP restriction triggered
  • **401.2 - Unauthorized**: Authentication failure (different from 403)