Powershell Automating Meraki Access Points with 802.1x NPS Policy


Wifi networks are critical aspects of any organization's infrastructure especially when 802.1x is deployed that manages a Wifi SSID, one of the common challenges network administrators face is maintaining an up-to-date inventory of access points and their IP addresses, especially when integrating with 802.1x Network Policy Servers (NPS). In this article, I'll explain how to leverage the Meraki Dashboard API to automate this process.

Step 1 : Meraki API calls via Scripting.

If your organization's 802.1x authentication setup uses Wifi SSID's with Meraki devices then you need a comprehensive list of all Meraki access points across our networks, manually collecting this information would be time-consuming and error-prone, especially with dozens of access points spread across multiple sites and networks, not to mention there is no export option from the management website.

Understanding the Meraki API

The Meraki Dashboard API provides a programmatic interface to interact with the Meraki network infrastructure. Before diving into code, there's an important prerequisite: API access must be enabled in your Meraki dashboard.

To enable API access:

  1. Log into the Meraki Dashboard
  2. Navigate to Organization > Settings
  3. Under the API access section, you'll need to specify which IP addresses are allowed to access the API
    • Enter the external IP address of the server that will be running the script
    • There is no simple "Enable API" toggle - you must specifically allow access from particular IP addresses
  4. Generate an API key from your user profile

Important note: If you're using a federated account (e.g., SSO login), you will need to perform these steps with a non-federated Meraki account. Federated accounts often have limitations with API access, so a standard Meraki dashboard account is required for API operations.

Script Approach (replace the humans)

Here's how I approached the problem. The PowerShell script follows a hierarchical structure:

  1. First, retrieve all organizations your API key has access to
  2. For each organization, retrieve all networks
  3. For each network, get all devices
  4. Filter for access points (models starting with "MR")
  5. Extract the device name and IP address information
  6. Save the information to a file for later use with our NPS server

Key Code Snippets

Let's look at the core components:

Setting up the API connection

# API Key - Replace with your actual Meraki API Key
$apiKey = "YOUR_MERAKI_API_KEY"

# Base URL for Meraki API
$baseUrl = "https://api.meraki.com/api/v1"

# Headers for API requests
$headers = @{
    "X-Cisco-Meraki-API-Key" = $apiKey
    "Content-Type" = "application/json"
    "Accept" = "application/json"
}

The headers are critical - the X-Cisco-Meraki-API-Key header authenticates your requests, while the Accept header ensures proper response format.

Retrieving Organizations

# Step 1: Get all organizations
Write-Host "Retrieving organizations..." -ForegroundColor Yellow
$orgsUrl = "$baseUrl/organizations"
$organizations = Invoke-RestMethod -Method Get -Uri $orgsUrl -Headers $headers

This retrieves all organizations your API key has access to. If you're managing multiple organizations, this ensures you capture access points across all of them.

Getting Networks and Devices

# Function to retrieve all networks in the organization
function Get-MerakiNetworks {
    param (
        [string]$organizationId
    )
    
    $networksUrl = "$baseUrl/organizations/$organizationId/networks"
    $response = Invoke-RestMethod -Method Get -Uri $networksUrl -Headers $headers
    return $response
}

# Function to retrieve devices in a network
function Get-MerakiDevices {
    param (
        [string]$networkId
    )
    
    $devicesUrl = "$baseUrl/networks/$networkId/devices"
    $response = Invoke-RestMethod -Method Get -Uri $devicesUrl -Headers $headers
    return $response
}

These functions retrieve networks within organizations and devices within networks. I structured them as reusable functions to maintain clean code.

Filtering for Access Points

# Step 4: Filter for access points and get their details
$accessPoints = $devices | Where-Object { $_.model -like "MR*" }

foreach ($ap in $accessPoints) {
    if ($ap.lanIp) {
        $totalAccessPoints++
        $output = "Device Name: $($ap.name), IP Address: $($ap.lanIp), 
        Network: $($network.name), Organization: $($org.name)"
        Write-Host "    $output" -ForegroundColor Green
        $output | Out-File -FilePath $outputFile -Append
    }
}

The key filtering happens with Where-Object { $_.model -like "MR*" } which identifies Meraki access points by their model number prefix. This works regardless of the specific model (MR33, MR42, MR56, etc.).

Cross-Network Coverage

What makes this script powerful is that it works across all networks and templates in your Meraki organization. It doesn't matter if your access points are in different networks, bound to templates, or spread across multiple sites - the script traverses the entire hierarchy to find them all.

For our 802.1x implementation, having a comprehensive and accurate list of access points is essential. This automated approach ensures we don't miss any devices when configuring our NPS server policies.

Step 2 : Retrieving RADIUS Client Information from NPS Servers

Managing Network Policy Server (NPS) infrastructure requires regular validation of RADIUS client configurations. I recently tackled the challenge of remotely extracting this information from multiple NPS servers to ensure consistency and compliance across our network.

The Key Challenge: Finding the Right Data Source

The most critical part of this process was identifying the reliable method to extract RADIUS client information. After several attempts, I discovered that the netsh nps show client command provided the most consistent results:

# This is the core command that reliably extracts RADIUS client information
$netshOutput = netsh nps show client

This command outputs detailed information about each RADIUS client in a structured format that includes the client name, IP address, status, and vendor information.

Parsing the Output

The real work happened in parsing this command's output into usable objects:

# Parse the command output into structured objects
foreach ($line in $netshOutput) {
    # Check for a new client section
    if ($line -match "Name\s+=\s+(.+)") {
        # Save previous client if we have one
        if ($currentClient) {
            $clients += $currentClient
        }
        
        # Create new client
        $clientName = $matches[1].Trim()
        
        $currentClient = [PSCustomObject]@{
            ServerName = $env:COMPUTERNAME
            FriendlyName = $clientName
            IPAddress = ""
            DeviceManufacturer = "RADIUS Standard" # Default
            Status = "Enabled" # Default
        }
    }
    # Check for IP address
    elseif ($currentClient -and $line -match "Address\s+=\s+(.+)") {
        $currentClient.IPAddress = $matches[1].Trim()
    }
    # Additional property matching...
}

Pitfalls I Encountered

My journey wasn't without challenges:

  1. Registry Queries Failed: Initially, I tried accessing the registry directly since NPS stores configuration there:
# This approach was unreliable - data structure varied between servers
$registryPath = "HKLM:\SYSTEM\CurrentControlSet\Services\RemoteAccess\Policy\Clients"
$clientKeys = Get-ChildItem -Path $registryPath

This approach failed because registry structures varied across different NPS versions and configurations.

  1. PowerShell Module Issues: I then tried using the NetworkPolicyServer PowerShell module:
# This failed when the module wasn't installed or loaded
Import-Module NetworkPolicyServer
$radiusClients = Get-NpsRadiusClient

This approach was inconsistent because the module wasn't automatically installed on all servers.

  1. CSV Export Problems: Even after getting the data, exporting to CSV was problematic:
# This would create empty files or files with missing data
$allResults | Export-Csv -Path $outputFile -NoTypeInformation

The solution was to construct the CSV manually:

# Building the CSV line by line worked reliably
$csvLines = @('"ServerName","FriendlyName","IPAddress","DeviceManufacturer","Status"')
foreach ($client in $allResults) {
    $line = '"{0}","{1}","{2}","{3}","{4}"' -f 
        $client.ServerName,
        $client.FriendlyName,
        $client.IPAddress,
        $client.DeviceManufacturer,
        $client.Status
    $csvLines += $line
}
Set-Content -Path $outputFile -Value $csvLines -Force

Remote Execution: The Key to Success

The breakthrough came when I executed the command remotely using PowerShell's Invoke-Command:

$serverResults = Invoke-Command -ComputerName $server -ScriptBlock {
    # Command execution and parsing logic here
}

This allowed me to run the commands directly on each NPS server, avoiding the need to install tools or modules on the management workstation.

Step 3 : Enhancing RADIUS Server Consistency for WPA2-Enterprise Authentication

We were WPA2-Enterprise for secure wireless authentication. The key difference you'll notice with this approach is that users see a username and password prompt when connecting, rather than entering a pre-shared key. This method provides several security advantages, including:

  • Individual user authentication
  • Centralized credential management
  • Detailed authentication logging
  • The ability to immediately revoke access for specific users

However, to ensure reliability and redundancy, multiple RADIUS servers were deployed. The challenge? Ensuring all servers have identical client configurations.

Powershell to compare the NPS configuration data

To solve this problem, I created a PowerShell script that compares RADIUS clients between two servers, focusing specifically on IP address consistency. This is crucial because if an access point's IP address is configured on one RADIUS server but not another, authentication failures can occur when the secondary server is used.

Here's the core comparison function:

function Get-ClientKey {
    param (
        [Parameter(Mandatory=$true)]
        [PSCustomObject]$Client
    )
    
    # Only use IP address as the key for comparison
    return "$($Client.IPAddress)"
}

function Compare-ServerClients {
    param (
        [Parameter(Mandatory=$true)]
        [hashtable]$ServerClients,
        
        [Parameter(Mandatory=$true)]
        [string]$Server1,
        
        [Parameter(Mandatory=$true)]
        [string]$Server2
    )
    
    $clients1 = $ServerClients[$Server1]
    $clients2 = $ServerClients[$Server2]
    
    # Create hashtables for client keys for easy comparison
    $clientKeys1 = @{}
    $clientKeys2 = @{}
    
    foreach ($client in $clients1) {
        $key = Get-ClientKey -Client $client
        $clientKeys1[$key] = $client
    }
    
    foreach ($client in $clients2) {
        $key = Get-ClientKey -Client $client
        $clientKeys2[$key] = $client
    }
    
    # Find differences
    $missingFromServer2 = @()
    $missingFromServer1 = @()
    
    foreach ($key in $clientKeys1.Keys) {
        if (-not $clientKeys2.ContainsKey($key)) {
            $missingFromServer2 += $clientKeys1[$key]
        }
    }
    
    foreach ($key in $clientKeys2.Keys) {
        if (-not $clientKeys1.ContainsKey($key)) {
            $missingFromServer1 += $clientKeys2[$key]
        }
    }
    
    return @{
        "MissingFromServer1" = $missingFromServer1
        "MissingFromServer2" = $missingFromServer2
    }
}

The key insight was to modify the comparison to focus exclusively on IP addresses. In the original implementation, the script was comparing both friendly names and IP addresses, which led to false discrepancies when only the names differed (but the actual network endpoints were identical).

How does it work?

The script works by:

  1. Importing a CSV file containing RADIUS client configurations
  2. Grouping clients by server
  3. Comparing IP addresses between servers
  4. Identifying missing clients on either server
  5. Providing detailed statistics on the findings

Implementation Details

The script uses a simple but effective approach to identify differences:

# Find differences
$missingFromServer2 = @()
$missingFromServer1 = @()

foreach ($key in $clientKeys1.Keys) {
    if (-not $clientKeys2.ContainsKey($key)) {
        $missingFromServer2 += $clientKeys1[$key]
    }
}

foreach ($key in $clientKeys2.Keys) {
    if (-not $clientKeys1.ContainsKey($key)) {
        $missingFromServer1 += $clientKeys2[$key]
    }
}

This approach efficiently identifies IP addresses that exist on one server but not the other, allowing administrators to quickly spot inconsistencies.

Running the Comparison

To run the comparison, I use commands like:

# Compare two specific servers
.\Compare-RadiusClients.ps1 -FilePath radiusclient.txt 
-Server1 "npx-mamager1" -Server2 "nps-manager2"

# Or compare one server against all others
.\Compare-RadiusClients.ps1 -FilePath radiusclient.txt -Server1 "nps-manager1"

Sample Output

The script provides clear, actionable output:

===== RADIUS Client Comparison Results (IP Address Only) =====
Server 1: nps-manager1 (140 entries)
Server 2: nps-manager2 (140 entries)

IP Addresses in nps-manager1 but not in nps-manager2: 0
IP Addresses in nps-manager2 but not in nps-manager1: 0

Servers have identical IP addresses!

===== Statistics =====
Total entries in nps-manager1: 140
Total entries in nps-manager2: 140
Common IP addresses: 140
IP address match percentage: 100%

Step 4 : Automating NPS RADIUS Client Configuration

The goal here is to keep this RADIUS client configurations synchronized across multiple Network Policy Servers (NPS) which manually is a critical but often tedious task. not when you have a PowerShell script to automate this process after identifying discrepancies between our NPS servers.

When running a comparison report between two NPS servers (nps-manager1 and nps-manger2), I discovered that two RADIUS clients existed on one server but were missing from the other, this kind of configuration drift can lead to authentication failures and frustrated users.

Here's what the comparison output looked like:

===== RADIUS Client Comparison Results (IP Address Only) =====
Server 1: nps-manager1 (152 entries)
Server 2: nps-manager2 (150 entries)

IP Addresses in nps-manager1 but not in nps-manager2: 2
IP Addresses in nps-manager2 but not in nps-manager1: 0

IP address discrepancies found!

P Addresses missing from nps-manager2
  - FriendlyName: Santa Cruz Office, IPAddress: 10.84.90.0/24, 
    DeviceManufacturer: RADIUS Standard
  - FriendlyName: San Jose Office, IPAddress: 10.91.44.102,
    DeviceManufacturer: RADIUS Standard

Rather than manually adding these missing entries, I decided to automate the process for
the Santa Cruz and San Jose offices in this example as one of the NPS servers would allow
the 802.1x connection and the other would deny it.

I created a PowerShell script that:

  1. Reads the comparison output file
  2. Identifies the target NPS server
  3. Extracts details of missing RADIUS clients
  4. Adds a random hex suffix to FriendlyNames to prevent duplication errors
  5. Provides an option to confirm changes before execution
  6. Uses a standardized shared secret

Key Script Components

Let's break down the important parts of the script:

Parameter Declaration

param(
    [string]$InputFile = "CompareNPSResults.txt",
    [string]$SharedSecret = "Meraki",
    [switch]$RequireConfirmation = $true
)

This section defines the script parameters:

  • $InputFile: Path to the comparison results file
  • $SharedSecret: The shared secret to use for all RADIUS clients (our standard is "Meraki")
  • $RequireConfirmation: A switch to control whether confirmation is required before changes

Random Hex Generator

To prevent duplicate name errors, I add a random 6-character hex suffix to each FriendlyName:

function Get-RandomHex {
    # Generate 6 random hex characters
    $hexChars = "0123456789ABCDEF"
    $result = ""
    for ($i = 0; $i -lt 6; $i++) {
        $result += $hexChars[(Get-Random -Minimum 0 -Maximum 16)]
    }
    return $result
}

Identifying the Target Server

The script extracts the target server name from the "IP Addresses missing from" line:

# Extract the target NPS server
if ($content -match "IP Addresses missing from ([^:]+):") {
    $targetServer = $matches[1].Trim()
    Write-Host "Target NPS Server: $targetServer" -ForegroundColor Green
} else {
    Write-Host "Error: Could not identify target NPS server in the input file." 
    -ForegroundColor Red
    exit 1
}

Parsing Missing Entries

Regular expressions help extract the details of each missing RADIUS client:

# Extract missing IP addresses information
$missingEntries = @()
$regex = "FriendlyName: ([^,]+), IPAddress: ([^,]+), DeviceManufacturer: ([^\r\n]+)"
$matches = [regex]::Matches($content, $regex)

foreach ($match in $matches) {
    $friendlyName = $match.Groups[1].Value.Trim()
    $ipAddress = $match.Groups[2].Value.Trim()
    $deviceManufacturer = $match.Groups[3].Value.Trim()
    
    # Append random hex to friendly name
    $uniqueFriendlyName = "$friendlyName-$(Get-RandomHex)"
    
    $missingEntries += [PSCustomObject]@{
        FriendlyName = $uniqueFriendlyName
        IPAddress = $ipAddress
        DeviceManufacturer = $deviceManufacturer
    }
}

Confirmation Mechanism

An important safety feature is the option to review and confirm changes:

# Display what will be added
Write-Host "`nThe following entries will be added to $($targetServer):" 
-ForegroundColor Cyan
$missingEntries | Format-Table -AutoSize

# Confirmation mechanism
$proceed = $true
if ($RequireConfirmation) {
    $response = Read-Host "Do you want to proceed with adding these entries? (Y/N)"
    $proceed = $response -eq "Y" -or $response -eq "y"
}

Adding the RADIUS Clients

Finally, the script adds each missing RADIUS client to the target server:

if ($proceed) {
    # Add each entry to the NPS server
    foreach ($entry in $missingEntries) {
        Write-Host "Adding $($entry.FriendlyName) with IP $($entry.IPAddress) 
        to $($targetServer)..." -ForegroundColor Yellow
        
        try {
            # Use the NPS PowerShell cmdlets to add the client
            $params = @{
                Name = $entry.FriendlyName
                Address = $entry.IPAddress
                SharedSecret = $SharedSecret
                ComputerName = $targetServer
            }
            
            # Uncomment this line in your environment
            # New-NpsRadiusClient @params
            
            Write-Host "Successfully added $($entry.FriendlyName)" 
           -ForegroundColor Green
        }
        catch {
            Write-Host "Error adding $($entry.FriendlyName): $_" -ForegroundColor Red
        }
    }
}

Lessons Learned: PowerShell Variable Expansion

While developing this script, I encountered an error related to variable expansion in PowerShell:

Variable reference is not valid. ':' was not followed by a valid variable name character.
Consider using ${} to delimit the name.

The problem occurred when using a variable followed immediately by punctuation:

# Problematic line
Write-Host "`nThe following entries will be added to $targetServer:" 
-ForegroundColor Cyan

PowerShell was interpreting the colon as part of the variable name. The solution was to use a subexpression $() to properly evaluate the variable:

# Fixed line
Write-Host "`nThe following entries will be added to $($targetServer):" 
-ForegroundColor Cyan

This is an important PowerShell best practice: always use the $() subexpression operator when a variable is directly followed by punctuation or when you need to include the result of an expression within a string.

Script Execution

To use the script in your environment:

  1. Run it with default settings (will require confirmation):
    .\Add-MissingNPSClients.ps1
    
  2. Run it without confirmation:
    .\Add-MissingNPSClients.ps1 -RequireConfirmation:$false
    
  3. Run it with a different shared secret:
    .\Add-MissingNPSClients.ps1 -SharedSecret "<secret-here>"
    

Conclusion

Automating RADIUS client configuration has saved me significant time and reduced the potential for human error. The script can be further extended to handle different comparison output formats or to run as a scheduled task.

Previous Post Next Post

نموذج الاتصال