Skip to main content
COSMICBYTEZLABS
NewsSecurityHOWTOsToolsStudyTraining
ProjectsChecklistsAI RankingsNewsletterStatusTagsAbout
Subscribe

Press Enter to search or Esc to close

News
Security
HOWTOs
Tools
Study
Training
Projects
Checklists
AI Rankings
Newsletter
Status
Tags
About
RSS Feed
Reading List
Subscribe

Stay in the Loop

Get the latest security alerts, tutorials, and tech insights delivered to your inbox.

Subscribe NowFree forever. No spam.
COSMICBYTEZLABS

Your trusted source for IT intelligence, cybersecurity insights, and hands-on technical guides.

429+ Articles
114+ Guides

CONTENT

  • Latest News
  • Security Alerts
  • HOWTOs
  • Projects
  • Exam Prep

RESOURCES

  • Search
  • Browse Tags
  • Newsletter Archive
  • Reading List
  • RSS Feed

COMPANY

  • About Us
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CosmicBytez Labs. All rights reserved.

System Status: Operational
  1. Home
  2. HOWTOs
  3. FortiGate Firewall Policy Management with PowerShell
FortiGate Firewall Policy Management with PowerShell
HOWTOAdvanced

FortiGate Firewall Policy Management with PowerShell

Automate FortiGate firewall policy creation, backup, and auditing using PowerShell and the FortiOS REST API. Includes bulk rule deployment, change...

Dylan H.

Network Security

February 3, 2026
7 min read

Prerequisites

  • FortiGate 6.4+
  • PowerShell 5.1+
  • API access enabled
  • Admin credentials

Overview

Managing firewall policies manually across multiple FortiGate devices doesn't scale. This guide covers automating policy management using PowerShell and the FortiOS REST API - from bulk rule deployment to compliance auditing.

What You'll Learn

  • FortiOS REST API authentication patterns
  • Bulk policy creation and modification
  • Configuration backup automation
  • Policy compliance auditing
  • Change tracking and rollback

Why Automate Firewall Management?

Manual ApproachAutomated Approach
Error-prone rule entryTemplated, validated rules
Inconsistent across devicesStandardized policies
No change trackingGit-backed versioning
Hours per deviceMinutes for entire fleet

API Authentication

FortiGate supports both API tokens and session-based authentication. API tokens are recommended for automation.

Generate API Token

  1. Navigate to System > Administrators
  2. Create new REST API Admin
  3. Set appropriate profile (read-only or read-write)
  4. Copy the generated token

PowerShell Authentication Module

function Connect-FortiGate {
    param(
        [Parameter(Mandatory)]
        [string]$Hostname,
 
        [Parameter(Mandatory)]
        [string]$ApiToken,
 
        [int]$Port = 443,
        [switch]$SkipCertificateCheck
    )
 
    $script:FGSession = @{
        BaseUrl = "https://${Hostname}:${Port}/api/v2"
        Headers = @{
            "Authorization" = "Bearer $ApiToken"
            "Content-Type"  = "application/json"
        }
        SkipCert = $SkipCertificateCheck
    }
 
    # Test connection
    try {
        $params = @{
            Uri     = "$($script:FGSession.BaseUrl)/cmdb/system/status"
            Headers = $script:FGSession.Headers
            Method  = "GET"
        }
 
        if ($SkipCertificateCheck) {
            $params.SkipCertificateCheck = $true
        }
 
        $response = Invoke-RestMethod @params
        Write-Host "Connected to $($response.results.hostname)" -ForegroundColor Green
        Write-Host "  Version: $($response.results.version)"
        Write-Host "  Serial:  $($response.results.serial)"
 
        return $true
    }
    catch {
        Write-Error "Connection failed: $_"
        return $false
    }
}

Firewall Policy Operations

List All Policies

function Get-FGFirewallPolicy {
    param(
        [string]$Name,
        [string]$VDOM = "root"
    )
 
    $uri = "$($script:FGSession.BaseUrl)/cmdb/firewall/policy"
 
    if ($Name) {
        $uri += "/$Name"
    }
 
    $params = @{
        Uri     = $uri
        Headers = $script:FGSession.Headers
        Method  = "GET"
    }
 
    if ($script:FGSession.SkipCert) {
        $params.SkipCertificateCheck = $true
    }
 
    $response = Invoke-RestMethod @params
    return $response.results
}
 
# Usage
$policies = Get-FGFirewallPolicy
$policies | Select-Object policyid, name, srcintf, dstintf, action | Format-Table

Create New Policy

function New-FGFirewallPolicy {
    param(
        [Parameter(Mandatory)]
        [string]$Name,
 
        [Parameter(Mandatory)]
        [string]$SourceInterface,
 
        [Parameter(Mandatory)]
        [string]$DestinationInterface,
 
        [string[]]$SourceAddress = @("all"),
        [string[]]$DestinationAddress = @("all"),
        [string[]]$Service = @("ALL"),
        [ValidateSet("accept", "deny", "ipsec")]
        [string]$Action = "accept",
        [switch]$NAT,
        [switch]$LogTraffic,
        [string]$Comment
    )
 
    $policy = @{
        name     = $Name
        srcintf  = @(@{ name = $SourceInterface })
        dstintf  = @(@{ name = $DestinationInterface })
        srcaddr  = $SourceAddress | ForEach-Object { @{ name = $_ } }
        dstaddr  = $DestinationAddress | ForEach-Object { @{ name = $_ } }
        service  = $Service | ForEach-Object { @{ name = $_ } }
        action   = $Action
        nat      = if ($NAT) { "enable" } else { "disable" }
        logtraffic = if ($LogTraffic) { "all" } else { "disable" }
    }
 
    if ($Comment) {
        $policy.comments = $Comment
    }
 
    $params = @{
        Uri         = "$($script:FGSession.BaseUrl)/cmdb/firewall/policy"
        Headers     = $script:FGSession.Headers
        Method      = "POST"
        Body        = $policy | ConvertTo-Json -Depth 10
    }
 
    if ($script:FGSession.SkipCert) {
        $params.SkipCertificateCheck = $true
    }
 
    $response = Invoke-RestMethod @params
    Write-Host "Policy '$Name' created with ID: $($response.mkey)" -ForegroundColor Green
    return $response
}

Bulk Policy Deployment

Deploy policies from a CSV template:

function Import-FGPoliciesFromCsv {
    param(
        [Parameter(Mandatory)]
        [string]$CsvPath
    )
 
    $policies = Import-Csv $CsvPath
    $results = @()
 
    foreach ($policy in $policies) {
        Write-Host "Creating policy: $($policy.Name)..."
 
        try {
            $result = New-FGFirewallPolicy `
                -Name $policy.Name `
                -SourceInterface $policy.SourceInterface `
                -DestinationInterface $policy.DestinationInterface `
                -SourceAddress ($policy.SourceAddress -split ",") `
                -DestinationAddress ($policy.DestinationAddress -split ",") `
                -Service ($policy.Service -split ",") `
                -Action $policy.Action `
                -NAT:([bool]$policy.NAT) `
                -LogTraffic:([bool]$policy.LogTraffic) `
                -Comment $policy.Comment
 
            $results += [PSCustomObject]@{
                Name   = $policy.Name
                Status = "Success"
                ID     = $result.mkey
            }
        }
        catch {
            $results += [PSCustomObject]@{
                Name   = $policy.Name
                Status = "Failed"
                Error  = $_.Exception.Message
            }
        }
    }
 
    return $results
}

CSV Template:

Name,SourceInterface,DestinationInterface,SourceAddress,DestinationAddress,Service,Action,NAT,LogTraffic,Comment
Allow-Web-DMZ,internal,dmz,LAN_Servers,Web_Servers,HTTP,HTTPS,accept,false,true,Web server access
Allow-DNS-Out,internal,wan1,all,DNS_Servers,DNS,accept,true,false,Outbound DNS
Block-Telnet,any,any,all,all,TELNET,deny,false,true,Security - No Telnet

Configuration Backup

Automated Backup Script

function Backup-FGConfiguration {
    param(
        [Parameter(Mandatory)]
        [string]$OutputPath,
 
        [ValidateSet("full", "vdom")]
        [string]$Scope = "full",
 
        [switch]$IncludeTimestamp
    )
 
    $filename = "fortigate-backup"
 
    if ($IncludeTimestamp) {
        $timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
        $filename = "${filename}-${timestamp}"
    }
 
    $uri = "$($script:FGSession.BaseUrl)/monitor/system/config/backup"
    $uri += "?scope=$Scope"
 
    $params = @{
        Uri     = $uri
        Headers = $script:FGSession.Headers
        Method  = "GET"
        OutFile = Join-Path $OutputPath "${filename}.conf"
    }
 
    if ($script:FGSession.SkipCert) {
        $params.SkipCertificateCheck = $true
    }
 
    Invoke-RestMethod @params
 
    $backupFile = Join-Path $OutputPath "${filename}.conf"
    $size = (Get-Item $backupFile).Length / 1KB
 
    Write-Host "Backup saved: $backupFile ($([math]::Round($size, 2)) KB)" -ForegroundColor Green
 
    return $backupFile
}

Scheduled Backup with Git Versioning

function Invoke-FGBackupWithGit {
    param(
        [Parameter(Mandatory)]
        [string]$BackupRepo,
 
        [string]$CommitMessage
    )
 
    Push-Location $BackupRepo
 
    # Perform backup
    $backupFile = Backup-FGConfiguration -OutputPath $BackupRepo
 
    # Git operations
    git add $backupFile
 
    $changes = git status --porcelain
    if ($changes) {
        $msg = if ($CommitMessage) {
            $CommitMessage
        } else {
            "Automated backup - $(Get-Date -Format 'yyyy-MM-dd HH:mm')"
        }
 
        git commit -m $msg
        Write-Host "Changes committed to Git" -ForegroundColor Green
    }
    else {
        Write-Host "No configuration changes detected" -ForegroundColor Yellow
    }
 
    Pop-Location
}

Policy Compliance Auditing

Security Baseline Check

function Test-FGPolicyCompliance {
    param(
        [switch]$Detailed
    )
 
    $policies = Get-FGFirewallPolicy
    $findings = @()
 
    foreach ($policy in $policies) {
        # Check: Any-to-Any rules
        $srcAll = $policy.srcaddr.name -contains "all"
        $dstAll = $policy.dstaddr.name -contains "all"
        $svcAll = $policy.service.name -contains "ALL"
 
        if ($srcAll -and $dstAll -and $svcAll -and $policy.action -eq "accept") {
            $findings += [PSCustomObject]@{
                PolicyID    = $policy.policyid
                PolicyName  = $policy.name
                Finding     = "CRITICAL: Any-to-Any-All rule"
                Severity    = "Critical"
                Remediation = "Restrict source, destination, or services"
            }
        }
 
        # Check: No logging on allow rules
        if ($policy.action -eq "accept" -and $policy.logtraffic -eq "disable") {
            $findings += [PSCustomObject]@{
                PolicyID    = $policy.policyid
                PolicyName  = $policy.name
                Finding     = "WARNING: Allow rule without logging"
                Severity    = "Medium"
                Remediation = "Enable traffic logging"
            }
        }
 
        # Check: Disabled policies
        if ($policy.status -eq "disable") {
            $findings += [PSCustomObject]@{
                PolicyID    = $policy.policyid
                PolicyName  = $policy.name
                Finding     = "INFO: Disabled policy"
                Severity    = "Low"
                Remediation = "Remove or re-enable policy"
            }
        }
 
        # Check: Missing comments
        if ([string]::IsNullOrWhiteSpace($policy.comments)) {
            $findings += [PSCustomObject]@{
                PolicyID    = $policy.policyid
                PolicyName  = $policy.name
                Finding     = "INFO: No policy description"
                Severity    = "Low"
                Remediation = "Add descriptive comment"
            }
        }
    }
 
    # Summary
    Write-Host "`nCompliance Summary" -ForegroundColor Cyan
    Write-Host "==================" -ForegroundColor Cyan
    Write-Host "Total Policies: $($policies.Count)"
    Write-Host "Critical: $(($findings | Where-Object Severity -eq 'Critical').Count)" -ForegroundColor Red
    Write-Host "Medium:   $(($findings | Where-Object Severity -eq 'Medium').Count)" -ForegroundColor Yellow
    Write-Host "Low:      $(($findings | Where-Object Severity -eq 'Low').Count)" -ForegroundColor Gray
 
    if ($Detailed) {
        return $findings
    }
 
    return $findings | Where-Object { $_.Severity -in @("Critical", "Medium") }
}

VPN Configuration

Site-to-Site IPsec VPN

function New-FGIPsecVPN {
    param(
        [Parameter(Mandatory)]
        [string]$Name,
 
        [Parameter(Mandatory)]
        [string]$RemoteGateway,
 
        [Parameter(Mandatory)]
        [string]$PreSharedKey,
 
        [Parameter(Mandatory)]
        [string]$LocalSubnet,
 
        [Parameter(Mandatory)]
        [string]$RemoteSubnet,
 
        [string]$Interface = "wan1"
    )
 
    # Phase 1
    $phase1 = @{
        name          = $Name
        type          = "static"
        interface     = $Interface
        "remote-gw"   = $RemoteGateway
        psksecret     = $PreSharedKey
        proposal      = "aes256-sha256"
        dhgrp         = "14"
        nattraversal  = "enable"
    }
 
    $params = @{
        Uri     = "$($script:FGSession.BaseUrl)/cmdb/vpn.ipsec/phase1-interface"
        Headers = $script:FGSession.Headers
        Method  = "POST"
        Body    = $phase1 | ConvertTo-Json -Depth 5
    }
 
    if ($script:FGSession.SkipCert) {
        $params.SkipCertificateCheck = $true
    }
 
    Invoke-RestMethod @params
    Write-Host "Phase 1 created" -ForegroundColor Green
 
    # Phase 2
    $phase2 = @{
        name        = "${Name}_p2"
        "phase1name" = $Name
        proposal    = "aes256-sha256"
        "src-subnet" = $LocalSubnet
        "dst-subnet" = $RemoteSubnet
    }
 
    $params.Uri = "$($script:FGSession.BaseUrl)/cmdb/vpn.ipsec/phase2-interface"
    $params.Body = $phase2 | ConvertTo-Json -Depth 5
 
    Invoke-RestMethod @params
    Write-Host "Phase 2 created" -ForegroundColor Green
 
    Write-Host "VPN '$Name' configured successfully" -ForegroundColor Cyan
}

Best Practices

  1. Use API tokens - More secure than storing passwords
  2. Enable logging - All allow rules should log traffic
  3. Document policies - Every rule needs a comment
  4. Version control - Git-back your configurations
  5. Audit regularly - Run compliance checks weekly
  6. Test in lab - Never deploy untested policies to production

Troubleshooting

API Connection Issues

# Test connectivity
Test-NetConnection -ComputerName "fortigate.local" -Port 443
 
# Check API status
curl -k "https://fortigate.local/api/v2/cmdb/system/status" `
     -H "Authorization: Bearer YOUR_TOKEN"

Common Error Codes

CodeMeaning
-5Object already exists
-6Object not found
-10Permission denied
-15Invalid parameter

Next Steps

  • SentinelOne Threat Hunting
  • Enterprise BitLocker Automation
  • Building a Secure Homelab
#FortiGate#Fortinet#firewall#PowerShell#api#Security

Related Articles

FortiGate Security Hardening: Best Practices for Enterprise

Complete FortiGate hardening guide covering admin access lockdown, firmware management, interface hardening, DNS/NTP security, certificate management,...

31 min read

SentinelOne Health Check: Agent Status Monitoring and

Organizations deploying SentinelOne endpoint protection require continuous monitoring of agent health to ensure comprehensive threat coverage across their...

17 min read

Deploy SentinelOne Policy

Deploy, manage, and validate SentinelOne security policies across your endpoint estate using the SentinelOne Management API. This automated workflow supports:

25 min read
Back to all HOWTOs