Sending Nagios Alerts to Microsoft Teams Without Webhooks
A Modern PowerShell & App Registration Approach for Nagios
Sending Nagios Alerts to Microsoft Teams Without Webhooks
A Modern PowerShell & App Registration Approach for Nagios
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:
- Why we chose to avoid webhooks
- What you need in Azure Portal and how to configure it
- The PowerShell scripts and how they work
- Requirements for sending messages to channels and chats
- 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.SendGroup.ReadWrite.All
For Teams Chat Messages:
Chat.ReadWriteChatMessage.SendUser.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)
- Nagios executes the script using command definitions.
- The PowerShell script retrieves an OAuth2 token from Azure AD.
- The message body is created using JSON:
{
"body": {
"content": "Nagios Alert: HOST/SERVICE STATUS..."
}
}
- Script sends the data using
Invoke-RestMethodto Graph API. - 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 = "✅"
$color = "#28a745" # Green
}
"WARNING" {
$icon = "⚠️"
$color = "#ffc107" # Yellow
}
"CRITICAL" {
$icon = "🚨"
$color = "#dc3545" # Red
}
default {
$icon = "💬"
$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 = "✅"
$color = "#28a745" # Green
}
"WARNING" {
$icon = "⚠️"
$color = "#ffc107" # Yellow
}
"CRITICAL" {
$icon = "🚨"
$color = "#dc3545" # Red
}
default {
$icon = "💬"
$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