← Back to list

Sending Nagios Alerts to Microsoft Teams Without Webhooks

A Modern PowerShell & App Registration Approach for Nagios

Firat Gulec · 2025-12-06 17:06 · 1 claps · 7.3 min read
#nagios #microsoft-teams #notifications #monitoring #security
Open on Medium ↗

Sending Nagios Alerts to Microsoft Teams Without Webhooks

A Modern PowerShell & App Registration Approach for Nagios

Photo by Ed Hardie on Unsplash

Photo by Ed Hardie on Unsplash

Introduction

Nagios has long been the backbone of infrastructure monitoring. However, as teams increasingly rely on real-time communication platforms like Microsoft Teams, classic email notifications often fall short. Many organizations hesitate to use incoming webhooks due to security restrictions, auditing concerns, or strict governance policies.

In my case, we needed Nagios to send alerts directly to Teams without using webhook URLs. Instead, we wanted a secure, Azure AD-based method using App Registration + Microsoft Graph API.

To achieve this, I created two PowerShell scripts:

  • One sends Nagios alerts to a Teams Channel
  • One sends Nagios alerts to a Teams Chat

In this article, I will walk you through:

  1. Why we chose to avoid webhooks
  2. What you need in Azure Portal and how to configure it
  3. The PowerShell scripts and how they work
  4. Requirements for sending messages to channels and chats
  5. How to integrate the scripts into Nagios using NConf Let’s begin.

Why Avoid Using Webhooks?

While Teams Incoming Webhooks are simple, they come with limitations:

  • Webhook URLs must be exposed in scripts or environment variables
  • Limited auditing — webhook actions don’t appear in Azure AD sign-in logs
  • Hard to rotate or restrict programmatically
  • Some enterprise security policies completely block webhook usage
  • No conditional access controls

By instead using an App Registration + Microsoft Graph, we gain:

  • Centralized Azure AD authentication
  • Conditional Access support
  • Token expiration and refresh control
  • Logging of every API call in Azure
  • Secure permissions management
  • No public URLs stored anywhere

For enterprises, this is a massive win.

Azure Portal Requirements (Step-by-Step)

To send Teams alerts using Graph API, we need:

1. Azure App Registration

Go to: Azure Portal → Azure Active Directory → App Registrations → New Registration

Give it a name, e.g.: NagiosTeamsIntegration Choose:

  • Supported account type: Single Tenant (recommended)

After creation, note:

  • Application (client) ID
  • Directory (tenant) ID

You’ll need them in the script.

2. Create a Client Secret

Under the App panel: Certificates & Secrets → New Client Secret → Add Save the secret securely — you will need it in your PowerShell scripts.

3. API Permissions

We need Microsoft Graph delegated permissions: For Teams Channel Messages:

  • ChannelMessage.Send
  • Group.ReadWrite.All

For Teams Chat Messages:

  • Chat.ReadWrite
  • ChatMessage.Send
  • User.Read

After adding these permissions: Click Grant admin consent Without this, your scripts will not authenticate properly.

PowerShell Scripts Explained

You mentioned two scripts — one for Teams Channel, one for Chat. Both authenticate using:

client_id
client_secret
tenant_id

Then they retrieve an access token from:<https://login.microsoftonline.com/$tenant_id/oauth2/v2.0/token>

And send the formatted Nagios alert to Microsoft Graph using:

  • /teams/{teamId}/channels/{channelId}/messages
  • /chats/{chatId}/messages

Channel Script Requirements

You must know: Teams ID, Channel ID These can be retrieved using Graph Explorer or PowerShell.

Chat Script Requirements

You must know:

  • The chat’s ID, or
  • The user IDs in a 1:1 or group chat

The script then formats the Nagios alert: Host name, Service description, Status, Time, Additional info and sends it to Teams.

How the Scripts Work (Technical Explanation)

  1. Nagios executes the script using command definitions.
  2. The PowerShell script retrieves an OAuth2 token from Azure AD.
  3. The message body is created using JSON:
{
  "body": {
     "content": "Nagios Alert: HOST/SERVICE STATUS..."
  }
}
  1. Script sends the data using Invoke-RestMethod to Graph API.
  2. Microsoft Teams receives and displays the alert. This creates a secure, scalable, webhook-free messaging pipeline. NagiosTeamsChatNotifier.ps1
#Send Teams Chat
param(
        [string]$HOSTNAME,
        [string]$SERVICEDESC,
        [string]$SERVICESTATE,
        [string]$OUTPUT
)
$tenantId = "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxx"
$clientId = "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxx"
$scopes = "offline_access Chat.ReadWrite User.Read"
$clientSecret = "xxxxx~xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxx"
$tokenFile  = "/usr/local/nagios/libexec/.nagios_graph_refresh"
$chatId = "19:meeting_xxxxxxxxxxxxxxxxxxxxxxxx@thread.v2"
# ---------- Functions ----------
function Save-RefreshToken($refreshToken, $path) {
    [System.IO.File]::WriteAllText($path, $refreshToken)
     #sudo chown nagios:nagios $path
     #sudo chmod 600 $path
}
function Load-RefreshToken($path) {
    if (-not (Test-Path $path)) { return $null }
    return Get-Content -Path $path -Raw
}
function Get-Token-By-Refresh($refreshToken) {
    $resp = Invoke-RestMethod -Method Post -Uri "<https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token>" -Body @{
        client_id    = $clientId
        grant_type   = "refresh_token"
        refresh_token= $refreshToken
        scope        = $scopes
    } -ErrorAction Stop
    return $resp
}
function Do-DeviceCodeFlow() {
    $deviceCodeResponse = Invoke-RestMethod -Method Post -Uri "<https://login.microsoftonline.com/$tenantId/oauth2/v2.0/devicecode>" -Body @{
        client_id = $clientId
        scope     = $scopes
    }
    Write-Host $deviceCodeResponse.message -ForegroundColor Yellow
    $accessToken = $null
    $interval = [int]$deviceCodeResponse.interval
    $expires_in = [int]$deviceCodeResponse.expires_in
    $start = Get-Date
    while (-not $accessToken) {
        Start-Sleep -Seconds $interval
        try {
            $tokenResponse = Invoke-RestMethod -Method Post -Uri "<https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token>" -Body @{
                grant_type  = "urn:ietf:params:oauth:grant-type:device_code"
                client_id   = $clientId
                device_code = $deviceCodeResponse.device_code
            } -ErrorAction Stop
            if ($tokenResponse.access_token) {
                return $tokenResponse
            }
        } catch {
            # authorization_pending or slow_down
            if ((Get-Date) -gt $start.AddSeconds($expires_in)) {
                throw "Device code timeout.."
            }
        }
    }
}
# ---------- Get Token ----------
try {
    $saved = Load-RefreshToken -path $tokenFile
    if ($saved) {
        Write-Host "Saved refresh token found, I will try refreshing..." -ForegroundColor Cyan
        try {
            $tokenResp = Get-Token-By-Refresh -refreshToken $saved
        } catch {
            Write-Warning "Could not get token with Refresh: $_. Will fallback to device code."
            $tokenResp = $null
        }
    } else {
        $tokenResp = $null
    }
    # 2) refresh is not working get refresh_token by device_code (Refresh Error)
    if (-not $tokenResp) {
        Write-Host "You need to log in with your device code..." -ForegroundColor Yellow
        $tokenResp = Do-DeviceCodeFlow
        # device code response's refresh_token (offline_access scope)
        if (-not $tokenResp.refresh_token) {
            throw "No refresh_token was received as a result of the device flow. Check the offline_access scope and application permissions."
        }
        # Save
        Save-RefreshToken -refreshToken $tokenResp.refresh_token -path $tokenFile
        Write-Host "Refresh token saved: $tokenFile" -ForegroundColor Green
    }
    $accessToken = $tokenResp.access_token
    # If tokenResp has new refresh_token (rotation), It'll be save
    if ($tokenResp.refresh_token -and $tokenResp.refresh_token -ne $saved) {
        Save-RefreshToken -refreshToken $tokenResp.refresh_token -path $tokenFile
        Write-Host "Refresh token updated." -ForegroundColor Green
    }
# ---------- Send message ---------- #
$time = Get-Date -Format 'u'
# Select Icons and colors 
switch ($SERVICESTATE) {
    "OK" {
        $icon = "&#x2705;"
        $color = "#28a745"  # Green
    }
    "WARNING" {
        $icon = "&#x26A0;&#xFE0F;"
        $color = "#ffc107"  # Yellow
    }
    "CRITICAL" {
        $icon = "&#x1F6A8;"
        $color = "#dc3545"  # Red
    }
    default {
        $icon = "&#x1F4AC;"
        $color = "#6c757d"  # Gray
    }
}
# HTML content
$htmlContent = @"
<div style='font-family:Segoe UI, Arial, sans-serif; padding:12px; border-left:5px solid $color; background:#f9f9f9; border-radius:6px;'>
  <h2 style='margin-top:0; color:$color;'>$icon Nagios Alert - $SERVICESTATE</h2>
  <p style='margin:6px 0;'><b>Host:</b> $HOSTNAME</p>
  <p style='margin:6px 0;'><b>Service:</b> $SERVICEDESC</p>
  <p style='margin:6px 0;'><b>Output:</b> $OUTPUT</p>
  <p style='margin-top:10px; font-size:12px; color:#777;'>⏰ $time</p>
</div>
"@
# Teams JSON body
$messageBody = @{
    body = @{
        contentType = "html"
        content = $htmlContent
    }
} | ConvertTo-Json -Depth 4
    $resp = Invoke-RestMethod -Method Post -Uri "<https://graph.microsoft.com/v1.0/chats/$chatId/messages>" `
        -Headers @{ Authorization = "Bearer $accessToken"; "Content-Type" = "application/json" } `
        -Body $messageBody -ErrorAction Stop
    Write-Host "Message has been sent. ID: $($resp.id)" -ForegroundColor Green
} catch {
    Write-Error "Error: $_"
}

NagiosTeamsChannelNotifier.ps1

# Send Teams Channel
param(
        [string]$HOSTNAME,
        [string]$SERVICEDESC,
        [string]$SERVICESTATE,
        [string]$OUTPUT
)
$tenantId = "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxx"
$clientId = "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxx"
$scopes = "offline_access Chat.ReadWrite User.Read"
$clientSecret = "xxxxxxxx~xxxxxxxxx-xxxxxxxxxxxxxxxxxxx"
$tokenFile  = "/usr/local/nagios/libexec/.nagios_graph_refresh"
# Teams Channel
$teamId    = "xxxxxx-xxxxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx"
$channelId = "19:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@thread.tacv2"
# ---------- Functions ----------
function Save-RefreshToken($refreshToken, $path) {
    [System.IO.File]::WriteAllText($path, $refreshToken)
     #sudo chown nagios:nagios $path
     #sudo chmod 600 $path
}
function Load-RefreshToken($path) {
    if (-not (Test-Path $path)) { return $null }
    return Get-Content -Path $path -Raw
}
function Get-Token-By-Refresh($refreshToken) {
    $resp = Invoke-RestMethod -Method Post -Uri "<https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token>" -Body @{
        client_id    = $clientId
        grant_type   = "refresh_token"
        refresh_token= $refreshToken
        scope        = $scopes
    } -ErrorAction Stop
    return $resp
}
function Do-DeviceCodeFlow() {
    $deviceCodeResponse = Invoke-RestMethod -Method Post -Uri "<https://login.microsoftonline.com/$tenantId/oauth2/v2.0/devicecode>" -Body @{
        client_id = $clientId
        scope     = $scopes
    }
    Write-Host $deviceCodeResponse.message -ForegroundColor Yellow
    $accessToken = $null
    $interval = [int]$deviceCodeResponse.interval
    $expires_in = [int]$deviceCodeResponse.expires_in
    $start = Get-Date
    while (-not $accessToken) {
        Start-Sleep -Seconds $interval
        try {
            $tokenResponse = Invoke-RestMethod -Method Post -Uri "<https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token>" -Body @{
                grant_type  = "urn:ietf:params:oauth:grant-type:device_code"
                client_id   = $clientId
                device_code = $deviceCodeResponse.device_code
            } -ErrorAction Stop
            if ($tokenResponse.access_token) {
                return $tokenResponse
            }
        } catch {
            # authorization_pending or slow_down
            if ((Get-Date) -gt $start.AddSeconds($expires_in)) {
                throw "Device code timeout.."
            }
        }
    }
}
# ---------- Get Token ----------
try {
    $saved = Load-RefreshToken -path $tokenFile
    if ($saved) {
        Write-Host "Saved refresh token found, I will try refreshing..." -ForegroundColor Cyan
        try {
            $tokenResp = Get-Token-By-Refresh -refreshToken $saved
        } catch {
            Write-Warning "Could not get token with Refresh: $_. Will fallback to device code."
            $tokenResp = $null
        }
    } else {
        $tokenResp = $null
    }
    # 2) refresh is not working get refresh_token by device_code (Refresh Error)
    if (-not $tokenResp) {
        Write-Host "You need to log in with your device code..." -ForegroundColor Yellow
        $tokenResp = Do-DeviceCodeFlow
        # device code response's refresh_token (offline_access scope)
        if (-not $tokenResp.refresh_token) {
            throw "No refresh_token was received as a result of the device flow. Check the offline_access scope and application permissions."
        }
        # Save
        Save-RefreshToken -refreshToken $tokenResp.refresh_token -path $tokenFile
        Write-Host "Refresh token saved: $tokenFile" -ForegroundColor Green
    }
    $accessToken = $tokenResp.access_token
    # If tokenResp has new refresh_token (rotation), It'll be save
    if ($tokenResp.refresh_token -and $tokenResp.refresh_token -ne $saved) {
        Save-RefreshToken -refreshToken $tokenResp.refresh_token -path $tokenFile
        Write-Host "Refresh token updated." -ForegroundColor Green
    }
# ---------- Send message ---------- #
$time = Get-Date -Format 'u'
# Select colors and icons
switch ($SERVICESTATE) {
    "OK" {
        $icon = "&#x2705;"
        $color = "#28a745"  # Green
    }
    "WARNING" {
        $icon = "&#x26A0;&#xFE0F;"
        $color = "#ffc107"  # Yellow
    }
    "CRITICAL" {
        $icon = "&#x1F6A8;"
        $color = "#dc3545"  # Red
    }
    default {
        $icon = "&#x1F4AC;"
        $color = "#6c757d"  # Gray
    }
}
# HTML content
$htmlContent = @"
<div style='font-family:Segoe UI, Arial, sans-serif; padding:12px; border-left:5px solid $color; background:#f9f9f9; border-radius:6px;'>
  <h2 style='margin-top:0; color:$color;'>$icon Nagios Alert - $SERVICESTATE</h2>
  <p style='margin:6px 0;'><b>Host:</b> $HOSTNAME</p>
  <p style='margin:6px 0;'><b>Service:</b> $SERVICEDESC</p>
  <p style='margin:6px 0;'><b>Output:</b> $OUTPUT</p>
  <p style='margin-top:10px; font-size:12px; color:#777;'>⏰ $time</p>
</div>
"@
# Teams JSON body
$messageBody = @{
    body = @{
        contentType = "html"
        content = $htmlContent
    }
} | ConvertTo-Json -Depth 4
    $resp = Invoke-RestMethod -Method Post -Uri "<https://graph.microsoft.com/v1.0/teams/$teamId/channels/$channelId/messages>" `
        -Headers @{ Authorization = "Bearer $accessToken"; "Content-Type" = "application/json" } `
        -Body $messageBody -ErrorAction Stop
    Write-Host "Message has been sent. ID: $($resp.id)" -ForegroundColor Green
} catch {
    Write-Error "Error: $_"
}

Integrating with Nagios via NConf

Now that the scripts work, let’s integrate them into Nagios.

Step 1: Create a Nagios Command in NConf

Go to: NConf → Commands → Add Command For host alerts:

/usr/bin/pwsh /usr/local/nagios/scripts/send-teams-host.ps1 \\
  -HostName "$HOSTNAME$" \\
  -HostState "$HOSTSTATE$" \\
  -Output "$HOSTOUTPUT$"

For service alerts:

/usr/bin/pwsh /usr/local/nagios/scripts/send-teams-service.ps1 \\
  -HostName "$HOSTNAME$" \\
  -ServiceName "$SERVICEDESC$" \\
  -ServiceState "$SERVICESTATE$" \\
  -Output "$SERVICEOUTPUT$"

Save the commands.

Step 2: Create a Contact in NConf

Go to: NConf → Contacts → Add Contact

Create a contact named: Teams_Notifications Set notification methods to the commands you created.

Step 3: Attach Contact to Contactgroup

Create a new group: teams-notification-group Add the contact to this group.

Step 4: Assign Contactgroup to Hosts/Services

Under:

  • Host templates
  • Service templates

Add the contact group so all alerts send to Teams automatically. That’s it.

Feel free to check out the scripts on GitHub and contribute your improvements or suggestions.


메타데이터
post_id
1d70b78a5b3f
slug
sending-nagios-alerts-to-microsoft-teams-without-webhooks-1d70b78a5b3f
url
https://medium.com/@firat-gulec/sending-nagios-alerts-to-microsoft-teams-without-webhooks-1d70b78a5b3f
canonical_url
https://medium.com/@firat-gulec/sending-nagios-alerts-to-microsoft-teams-without-webhooks-1d70b78a5b3f
author_url
https://medium.com/@firat-gulec
status
ok
fetched_at
2026-07-14 11:36:08