Skip to main content
Solved

Fortinet physical devices versus vdoms and validating tacacs configuration.

  • May 12, 2026
  • 7 replies
  • 79 views

Forum|alt.badge.img+2

Forward brings in Fortinet firewalls as the device its self and the VDOM’s this becomes challenging when you need to validate the tacacs configuration for the physical device this means quite often the vdoms or even the root vdom will fail even though tacacs is configured on the device.

The simple solution is to add a permitted violation list, however this needs to be maintained manually there must be a better solution surely?

userTacacs =
```
config user tacacs+
edit "TACACS"
set server [server 1 IP Address]
set secondary-server [server 2 IP Address]
set key ENC {string}
set secondary-key ENC {string}
set authorization enable
next
end
```;
systemAdmin =
```
config system admin
edit "tacacs"
set remote-auth enable
set accprofile "no_access"
set vdom {vdoms:string}
set wildcard enable
set remote-group "TACACS_ACCESS"
set accprofile-override enable

```;

foreach device in network.devices
where device.platform.vendor == Vendor.FORTINET

foreach command in device.outputs.commands
where command.commandText == "show user tacacs+" // Retrieve only the tacacs user.
let tacacsOutput = command.response
let tacacsConfig = parseConfigBlocks(OS.OTHER,tacacsOutput)
let userMatches = blockMatches(tacacsConfig, userTacacs)
let userResult = if length(userMatches) == 0
then "User tacacs+ configuration is not standard"
else ""
let sysAdminMatches = blockMatches(device.files.config,systemAdmin)
let sysAdminResult = if length(sysAdminMatches) == 0
then "System Admin tacacs configuration is not standard"
else ""
let result = if userResult != "" && sysAdminResult == ""
then userResult
else if userResult == "" && sysAdminResult != ""
then sysAdminResult
else if userResult != "" && sysAdminResult != ""
then userResult + " and "+sysAdminResult
else "No issues found"

select {
Device: device.name,
violation: length(userMatches) == 0 || length(sysAdminMatches) == 0,
Reason_for_violation:result,
User_Matches: (foreach match in userMatches
select match.blocks),
Admin_Matches: (foreach match in sysAdminMatches
select match.blocks)
}

 

Best answer by rob

Assuming the above works - here is some AI assisted refactoring to make it more readable :) 

 

userTacacs =
```
config user tacacs+
edit "TACACS"
set server [server 1 IP Address]
set secondary-server [server 2 IP Address]
set key ENC {string}
set secondary-key ENC {string}
set authorization enable
next
end
```;

systemAdmin =
```
config system admin
edit "tacacs"
set remote-auth enable
set accprofile "no_access"
set vdom {vdoms:string}
set wildcard enable
set remote-group "TACACS_ACCESS"
set accprofile-override enable
```;

foreach device in network.devices
where device.platform.vendor == Vendor.FORTINET
foreach command in device.outputs.commands
where command.commandText == "show user tacacs+" // Retrieve only the tacacs user.
let tacacsOutput = command.response
let tacacsConfig = parseConfigBlocks(OS.OTHER, tacacsOutput)
let userMatches = blockMatches(tacacsConfig, userTacacs)
let adminMatches = blockMatches(device.files.config, systemAdmin)
let userOk = length(userMatches) > 0
let adminOk = length(adminMatches) > 0
let reason = if userOk && adminOk
then ""
else if !userOk && !adminOk
then "User tacacs+ and System Admin tacacs configurations are not standard"
else if !userOk
then "User tacacs+ configuration is not standard"
else "System Admin tacacs configuration is not standard"
group { reason, userMatches, adminMatches } as results
by device.system.physicalName as physicalName
let passing = any(foreach r in results
select r.reason == "")
let violation = !passing
// If any logical device passes, report no issues; otherwise surface the first reason found
let finalReason = if !violation
then ""
else max(foreach r in results
select r.reason)
let anyUserMatches = (foreach r in results
foreach m in r.userMatches
select m.blocks)
let anyAdminMatches = (foreach r in results
foreach m in r.adminMatches
select m.blocks)
select {
Device: physicalName,
violation: violation,
Reason_for_violation: finalReason,
User_Matches: anyUserMatches,
Admin_Matches: anyAdminMatches
}

 

7 replies

Tyson Henrie
Employee
Forum|alt.badge.img+3

Steve,

My apologies I’m confused by your statement:

“The simple solution is to add a permitted violation list, however this needs to be maintained manually there must be a better solution surely?”  I’m not clear on what the manual permitted violation list is in the query.

For the “config user tacacs+” you might bet better off using blockDiff instead of blockMatches.  The blockDiff is a logical OR and the blockMatches is a logical AND.  It makes the blockDiff a bit more flexible.  However, blockDiff does not handle multiple instances in a single devices.

If I’m worried about a lot of variability I’ll fall back on the old patternMatches.  If there are only 2 or maybe 3 variants, then explicitly listing out those 2 or 3 patterns is not too bad.  But if you expect there could be more.  Then patternMatches can provide more flexibility.

That is one thing about Fortinet.  Their configurations don’t feel as well organized.

BTW the first pattern.  You have lines that I would consider as extra and not needed.  Like the “next” and the “end”.

 


Forum|alt.badge.img+2
  • Author
  • Spotter
  • May 13, 2026

@Tyson Henrie The issue is that for Fortinets the physical device and root vdom are one device and then any additional VDOMs are separate devices, we only have the tacacs configured in either the root domain or the VDOM for which 

Device Example from the inventory

So if we look at this example if “Firewall1” and therefore the root VDOM had the tacacs configuration then Firewall1_vdom which does not have tacacs configuration would fail, even though as a physical device it has Tacacs configured.

In the same token if “Firewall2_vdom” had the tacacs configuration then “Firewall2” would fail the check even though the physical device has tacacs configured and is therefore compliant.

I think I might have a solution and will work on it today, if it works I will share, but in the mean time if anyone has any ideas, would gratefully appreciate it 😀


rob
Employee
Forum|alt.badge.img+1
  • Employee
  • May 14, 2026

Hi ​@SteveBamford  - it’s been a while, I hope you are well :) 

You can actually tackle this by grouping your results using device.system.physicalName. Because this attribute is identical for the parent device and its VDOMs, it makes sorting them out pretty straightforward. Once you group them, you'll notice each physical device has 1 'pass' and multiple 'fails'. All you need to do is grab that 1 'pass' from the group and map it to the corresponding VDOM.

 

Does the below help or at least point you in the right direction?

 

userTacacs =
```
config user tacacs+
edit "TACACS"
set server [server 1 IP Address]
set secondary-server [server 2 IP Address]
set key ENC {string}
set secondary-key ENC {string}
set authorization enable
next
end
```;

systemAdmin =
```
config system admin
edit "tacacs"
set remote-auth enable
set accprofile "no_access"
set vdom {vdoms:string}
set wildcard enable
set remote-group "TACACS_ACCESS"
set accprofile-override enable
```;

foreach device in network.devices
where device.platform.vendor == Vendor.FORTINET
foreach command in device.outputs.commands
where command.commandText == "show user tacacs+" // Retrieve only the tacacs user.
let tacacsOutput = command.response
let tacacsConfig = parseConfigBlocks(OS.OTHER, tacacsOutput)
let userMatches = blockMatches(tacacsConfig, userTacacs)
let userResult = if length(userMatches) == 0
then "User tacacs+ configuration is not standard"
else ""
let sysAdminMatches = blockMatches(device.files.config, systemAdmin)
let sysAdminResult = if length(sysAdminMatches) == 0
then "System Admin tacacs configuration is not standard"
else ""
let reason = min([userResult, sysAdminResult])
group reason as reasons
by { physicalDevice: device.system.physicalName,
userMatches,
sysAdminMatches
}
as deviceInfo
let violation = min(reasons) != ""
select distinct{
Device: deviceInfo.physicalDevice,
violation,
Reason_for_violation: max(reasons),
User_Matches: (foreach match in deviceInfo.userMatches
select match.blocks),
Admin_Matches: (foreach match in deviceInfo.sysAdminMatches
select match.blocks)
}

 


rob
Employee
Forum|alt.badge.img+1
  • Employee
  • Answer
  • May 14, 2026

Assuming the above works - here is some AI assisted refactoring to make it more readable :) 

 

userTacacs =
```
config user tacacs+
edit "TACACS"
set server [server 1 IP Address]
set secondary-server [server 2 IP Address]
set key ENC {string}
set secondary-key ENC {string}
set authorization enable
next
end
```;

systemAdmin =
```
config system admin
edit "tacacs"
set remote-auth enable
set accprofile "no_access"
set vdom {vdoms:string}
set wildcard enable
set remote-group "TACACS_ACCESS"
set accprofile-override enable
```;

foreach device in network.devices
where device.platform.vendor == Vendor.FORTINET
foreach command in device.outputs.commands
where command.commandText == "show user tacacs+" // Retrieve only the tacacs user.
let tacacsOutput = command.response
let tacacsConfig = parseConfigBlocks(OS.OTHER, tacacsOutput)
let userMatches = blockMatches(tacacsConfig, userTacacs)
let adminMatches = blockMatches(device.files.config, systemAdmin)
let userOk = length(userMatches) > 0
let adminOk = length(adminMatches) > 0
let reason = if userOk && adminOk
then ""
else if !userOk && !adminOk
then "User tacacs+ and System Admin tacacs configurations are not standard"
else if !userOk
then "User tacacs+ configuration is not standard"
else "System Admin tacacs configuration is not standard"
group { reason, userMatches, adminMatches } as results
by device.system.physicalName as physicalName
let passing = any(foreach r in results
select r.reason == "")
let violation = !passing
// If any logical device passes, report no issues; otherwise surface the first reason found
let finalReason = if !violation
then ""
else max(foreach r in results
select r.reason)
let anyUserMatches = (foreach r in results
foreach m in r.userMatches
select m.blocks)
let anyAdminMatches = (foreach r in results
foreach m in r.adminMatches
select m.blocks)
select {
Device: physicalName,
violation: violation,
Reason_for_violation: finalReason,
User_Matches: anyUserMatches,
Admin_Matches: anyAdminMatches
}

 


Forum|alt.badge.img+2
  • Author
  • Spotter
  • May 18, 2026

Hi ​@rob definitely been a while, the first script seems to work a lot better, I am just adding some server checks and will feed back today hopefully.


Forum|alt.badge.img+2
  • Author
  • Spotter
  • May 21, 2026

Assuming the above works - here is some AI assisted refactoring to make it more readable :) 
 

So having spent some time validating I added the server checks to the later script as I could confirm it was working and now we have the correct output.

/**
* @intent Enter your intent here (one line, 50 characters max)
* @description Enter your description here (multiple lines are possible)
*/

tacacsServers = ["\"10.0.0.1\"","\"10.0.0.2\""]; //Tacacs Server Ips (Real IPs replaced)
userTacacs =
```
config user tacacs+
edit "TACACS"
set server {primaryServer:string}
set secondary-server {secondaryServer:string}
set key ENC {string}
set secondary-key ENC {string}
set authorization enable
```;

systemAdmin =
```
config system admin
edit "tacacs"
set remote-auth enable
set accprofile "no_access"
set vdom {vdoms:string}
set wildcard enable
set remote-group "TACACS_ACCESS"
set accprofile-override enable
```;
tacacsServerCheck (userMatches) =
foreach match in userMatches
let primaryServer = match.data.primaryServer
let secondaryServer = match.data.secondaryServer

let serverMatches = if primaryServer not in tacacsServers && secondaryServer not in tacacsServers
then "Primary tacacs server "+primaryServer+ " and secondary tacacs server "+secondaryServer+" are not compliant"
else if primaryServer not in tacacsServers && secondaryServer in tacacsServers
then "Primary tacacs server "+primaryServer+" is not compliant"
else if primaryServer in tacacsServers && secondaryServer not in tacacsServers
then "Seondary Tacacs server "+secondaryServer+" is not compliant"
else ""

select serverMatches
;

foreach device in network.devices
where device.platform.vendor == Vendor.FORTINET
foreach command in device.outputs.commands
where command.commandText == "show user tacacs+" // Retrieve only the tacacs user.
let tacacsOutput = command.response
let tacacsConfig = parseConfigBlocks(OS.OTHER, tacacsOutput)
let userMatches = blockMatches(tacacsConfig, userTacacs)
let serverMatches = tacacsServerCheck(userMatches)
let serverText = toString((foreach match in serverMatches select match))
let adminMatches = blockMatches(device.files.config, systemAdmin)

let userOk = length(userMatches) > 0
let adminOk = length(adminMatches) > 0
let serverOk = toString((foreach match in serverMatches
select match))
let serverOk = replace(serverOk,"[","")
let serverOk = replace(serverOk,"]","")
let lengthServer = length(serverMatches)
let reason = if !userOk && !adminOk && serverOk != ""
then "User tacacs+ and System Admin tacacs configurations are not standard as well as "+serverText
else if !userOk && adminOk && serverOk != ""
then "User tacacs+ configuration is not standard and "+serverText
else if userOk && !adminOk && serverOk != ""
then "System Admin tacacs configuration is not standard and "+serverText
else if userOk && adminOk && serverOk != ""
then serverText
else if !userOk && !adminOk && serverOk == ""
then "User tacacs+ and System Admin tacacs configurations are not standard as well as."
else if !userOk && adminOk && serverOk == ""
then "User tacacs+ configuration is not standard"
else if userOk && !adminOk && serverOk == ""
then "System Admin tacacs configuration is not standard"
else ""
group { reason, userMatches, adminMatches } as results
by device.system.physicalName as physicalName
let passing = any(foreach r in results
select r.reason == "")
let violation = !passing
// If any logical device passes, report no issues; otherwise surface the first reason found
let finalReason = if !violation
then ""
else max(foreach r in results
select r.reason)
let anyUserMatches = (foreach r in results
foreach m in r.userMatches
select m.blocks)
let anyAdminMatches = (foreach r in results
foreach m in r.adminMatches
select m.blocks)

select {
Device: physicalName,
violation: violation,
Reason_for_violation: finalReason,
User_Matches: anyUserMatches,
Admin_Matches: anyAdminMatches
}

 


rob
Employee
Forum|alt.badge.img+1
  • Employee
  • May 21, 2026

Very nice work Steve :)  Thanks for sharing the final version!