Skip to content

Automated Local Admin Removal via Tanium

Overview

This solution automatically removes domain accounts from the local Administrators group on all managed endpoints. It consists of two PowerShell scripts and two Tanium packages working together:

Component Role
Get-AdminOU.ps1 Runs on a remote server — queries AD, writes InAdminOU.txt, uploads it to Tanium Cloud, updates Package 2
Remove-AdminOUUsers.ps1 Runs on each endpoint via Tanium — reads InAdminOU.txt and removes listed accounts from local Administrators
Package 1 Calls Get-AdminOU.ps1 from a remote server (scheduled or on-demand)
Package 2 Delivers InAdminOU.txt + Remove-AdminOUUsers.ps1 to endpoints and executes the removal

Prerequisites

On a remote server

  • PowerShell 5.1+
  • RSAT ActiveDirectory module

Install RSAT if missing:

Add-WindowsCapability -Online -Name Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0

Or via DISM (Server OS):

Install-WindowsFeature -Name RSAT-AD-PowerShell

Tanium

  • API service account with permissions to upload files and update package 2
  • Package 2 (LabTest - Remove Admin for example) already created in the Tanium console before running Get-AdminOU.ps1 for the first time — note its numeric Package ID (148603 for example)

Step 1 — Create the Tanium API Credential File

This is a one-time setup step run interactively on the machine that will execute Get-AdminOU.ps1. The password is encrypted with DPAPI and is machine- and user-bound — no plaintext is stored.

$c = Get-Credential -UserName 'your-tanium-api-user' -Message 'Enter Tanium API password'
$c.Password | ConvertFrom-SecureString |
    Out-File 'C:\path\to\scripts\tanium_api_cred.bin'

Verify it was created:

Test-Path 'C:\path\to\scripts\tanium_api_cred.bin'
# Returns: True

Step 2 — Configure Get-AdminOU.ps1

Edit the config block at the top of the script to match your environment:

$TaniumUrl    = 'https://tanium.yourdomain.com'   # Your Tanium URL
$TaniumUser   = 'api-service-account'             # Your Tanium API username
$Package2Id   = 148603                            # Package ID of Package 2
$Package2Name = 'LabTest - Remove Admin'          # Display name of Package 2

$AdminOUs = @(
    'OU=Users,DC=yourdomain,DC=local'             # Replace with your OU
    # Add more OUs as needed:
    # 'OU=ServiceAccounts,OU=Users,DC=yourdomain,DC=local',
    # 'OU=PrivilegedUsers,DC=yourdomain,DC=local'
)

SSL bypass (lab environments)

If your Tanium server uses a self-signed or internal CA certificate, set this flag at the top of the script:

$BypassSslErrors = $true   # Set $false in production with a trusted cert

Step 3 — Get-AdminOU.ps1 (Full Script)

Save this as Get-AdminOU.ps1 in the same directory as tanium_api_cred.bin.

#Requires -Modules ActiveDirectory
<#
.SYNOPSIS
    1. Queries AD for all user accounts in admin OUs
    2. Writes InAdminOU.txt to the same directory
    3. Authenticates to Tanium via API
    4. Uploads InAdminOU.txt via streaming upload
    5. PATCHes Package 2 so all endpoints get the fresh file

.NOTES
    Credential pre-seeded as DPAPI SecureString (machine-bound, no plaintext).
    To generate once (run interactively on this machine):
        $c = Get-Credential
        $c.Password | ConvertFrom-SecureString |
            Out-File 'C:\path\to\scripts\tanium_api_cred.bin'
#>
[CmdletBinding()]
param()

$ErrorActionPreference = 'Stop'
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

# Lab SSL bypass - set $true if Tanium certificate is self-signed or from an internal CA.
# Set $false (or remove) once a trusted certificate is in place.
$BypassSslErrors = $true
if ($BypassSslErrors) {
    [Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
}

# --- Config ---------------------------------------------------------------
$ScriptDir   = if ($PSScriptRoot) {
                   $PSScriptRoot
               } elseif ($MyInvocation.MyCommand.Path) {
                   Split-Path -Parent $MyInvocation.MyCommand.Path
               } else {
                   Write-Warning "Script path unavailable - falling back to working directory. Save the script before running."
                   (Get-Location).Path
               }
$OutputFile  = Join-Path $ScriptDir 'InAdminOU.txt'
$LogFile     = Join-Path $ScriptDir 'Get-AdminOU.log'
$CredFile    = Join-Path $ScriptDir 'tanium_api_cred.bin'

$TaniumUrl    = 'https://tanium.yourdomain.com'   # Your Tanium URL
$TaniumUser   = 'api-service-account'             # Your Tanium API username
$Package2Id   = 148603                            # Package ID of package2
$Package2Name = 'LabTest - Remove Admin'          # Display name of package2

$AdminOUs = @(
    'OU=Users,DC=yourdomain,DC=local'             # Replace with OU
    # 'OU=ServiceAccounts,OU=SUsers,DC=yourdomain,DC=local',
    # 'OU=PrivilegedUsers,DC=yourdomain,DC=local'
)

function Write-Log {
    param([string]$Message, [string]$Level = 'INFO')
    $ts = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
    $line = "[$ts] [$Level] $Message"
    Add-Content -Path $LogFile -Value $line -Encoding UTF8
    Write-Host $line
}

# --- PHASE 1: Query AD and write InAdminOU.txt ----------------------------
try {
    Write-Log "=== Phase 1: AD Query ==="

    $results = [System.Collections.Generic.List[PSCustomObject]]::new()

    foreach ($ou in $AdminOUs) {
        Write-Log "Querying: $ou"
        try {
            $users = Get-ADUser -SearchBase $ou -Filter * -SearchScope Subtree `
                -Properties SamAccountName, DisplayName, EmailAddress, `
                            DistinguishedName, Enabled, PasswordLastSet, LastLogonDate

            foreach ($u in $users) { $results.Add($u) }
            Write-Log "  Found $($users.Count) accounts"
        }
        catch {
            Write-Log "WARNING: Could not query '$ou': $_" -Level 'WARN'
        }
    }

    if ($results.Count -eq 0) {
        Write-Log "No accounts found - aborting to avoid pushing empty file" -Level 'ERROR'
        exit 1
    }

    $header = 'SamAccountName'
    $lines  = [System.Collections.Generic.List[string]]::new()
    $lines.Add($header)

    foreach ($r in $results) {
        $lines.Add($r.SamAccountName)
    }

    $enc = [System.Text.UTF8Encoding]::new($false)   # UTF-8 no BOM
    [System.IO.File]::WriteAllLines($OutputFile, $lines, $enc)
    Write-Log "Wrote $($results.Count) accounts to $OutputFile"
}
catch {
    Write-Log "Phase 1 failed: $_" -Level 'ERROR'
    exit 1
}

# --- PHASE 2: Authenticate to Tanium --------------------------------------
try {
    Write-Log "=== Phase 2: Tanium Authentication ==="

    $secPwd   = Get-Content $CredFile | ConvertTo-SecureString
    $bstr     = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secPwd)
    $plainPwd = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr)
    [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)

    $loginResp = Invoke-RestMethod `
        -Uri         "$TaniumUrl/api/v2/session/login" `
        -Method      POST `
        -Body        (@{ username = $TaniumUser; password = $plainPwd; domain = '' } | ConvertTo-Json -Compress) `
        -ContentType 'application/json'

    $plainPwd = $null
    $session  = $loginResp.data.session
    $headers  = @{ session = $session }
    Write-Log "Authenticated - session acquired"
}
catch {
    Write-Log "Phase 2 failed: $_" -Level 'ERROR'
    exit 1
}

# --- PHASE 3: Upload InAdminOU.txt ----------------------------------------
try {
    Write-Log "=== Phase 3: Upload InAdminOU.txt ==="

    $fileBytes  = [System.IO.File]::ReadAllBytes($OutputFile)

    $uploadResp = Invoke-RestMethod `
        -Uri         "$TaniumUrl/api/v2/upload_file_stream" `
        -Method      POST `
        -Headers     $headers `
        -Body        $fileBytes `
        -ContentType 'application/octet-stream'

    $newHash = $uploadResp.data.hash
    Write-Log "Uploaded $($fileBytes.Length) bytes - hash: $newHash"
}
catch {
    Write-Log "Phase 3 failed: $_" -Level 'ERROR'
    try { Invoke-RestMethod -Uri "$TaniumUrl/api/v2/session/logout" -Method POST -Headers $headers -Body "{}" -ContentType "application/json" | Out-Null } catch {}
    exit 1
}

# --- PHASE 4: Update Package 2 ---------------------------------------------
try {
    Write-Log "=== Phase 4: Patch Package 2 (id $Package2Id) ==="

    $pkgResp = Invoke-RestMethod `
        -Uri     "$TaniumUrl/api/v2/packages/$Package2Id" `
        -Method  GET `
        -Headers $headers

    $keptFiles = @(
        $pkgResp.data.files |
            Where-Object { $_.name -and $_.name -ne 'InAdminOU.txt' } |
            ForEach-Object { @{ id = $_.id; name = $_.name } }
    )

    $updatedFiles = $keptFiles + @{ hash = $newHash; name = 'InAdminOU.txt' }

    $patchBody = @{
        files = $updatedFiles
    } | ConvertTo-Json -Depth 5 -Compress

    $patchResp = Invoke-RestMethod `
        -Uri         "$TaniumUrl/api/v2/packages/$Package2Id" `
        -Method      PATCH `
        -Headers     $headers `
        -Body        $patchBody `
        -ContentType 'application/json'

    Write-Log "Package 2 patched - modified: $($patchResp.data.modification_time)"
}
catch {
    Write-Log "Phase 4 failed: $_" -Level 'ERROR'
    try { Invoke-RestMethod -Uri "$TaniumUrl/api/v2/session/logout" -Method POST -Headers $headers -Body "{}" -ContentType "application/json" | Out-Null } catch {}
    exit 1
}

# --- Done -----------------------------------------------------------------
try { Invoke-RestMethod -Uri "$TaniumUrl/api/v2/session/logout" -Method POST -Headers $headers -Body "{}" -ContentType "application/json" | Out-Null } catch {}
Write-Log "Session logged out"
Write-Log "=== All phases complete ==="
exit 0

Manual Test - Expected output

[INFO] === Phase 1: AD Query ===
[INFO] Querying: OU=Users,DC=yourdomain,DC=local
[INFO]   Found 19 accounts
[INFO] Wrote 19 accounts to C:\...\InAdminOU.txt
[INFO] === Phase 2: Tanium Authentication ===
[INFO] Authenticated - session acquired
[INFO] === Phase 3: Upload InAdminOU.txt ===
[INFO] Uploaded 2212 bytes - hash: 12c8eb0f...
[INFO] === Phase 4: Patch Package 2 (id 148603) ===
[INFO] Package 2 patched - modified: 2026-05-28T18:57:52Z
[INFO] Session logged out
[INFO] === All phases complete ===

Step 4 — Create Tanium Package 1 (AD Query Package)

This package runs Get-AdminOU.ps1 on the designated runner server — this does not have to be the Domain Controller, just any domain-joined server with RSAT installed and the Tanium client running.

Directory layout on the runner server

All three files live at a fixed path on the runner server. F:\ represents the volume on that server. Nothing is uploaded to Tanium — the package is a trigger only:

F:\path\to\scripts\
    ├── Get-AdminOU.ps1       ← pre-existing on the server, referenced by the command
    ├── tanium_api_cred.bin   ← pre-seeded once manually (DPAPI encrypted)
    └── InAdminOU.txt         ← written by the script on each run

Package settings

In the Tanium console, go to Content → Packages → New Package:

Field Value
Package Name AD - Query Admin OU Users (for example)
Command cmd.exe /d /c %SystemRoot%\sysnative\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -NonInteractive -NoProfile -File "F:\path\to\scripts\Get-AdminOU.ps1"
Command Timeout 10 minutes
Files (none — all files are pre-existing on the runner server)

Replace F:\path\to\scripts\ with the actual path on your runner server.

Because $PSScriptRoot in the script resolves to F:\path\to\scripts\, both tanium_api_cred.bin and InAdminOU.txt are automatically found in the same directory — no hardcoded paths needed inside the script.


Step 5 — Create Tanium Package 2 (Enforcement Package)

This package delivers InAdminOU.txt to endpoints and runs the removal.

In the Tanium console, go to Content → Packages → New Package:

Field Value
Package Name LabTest - Remove Admin (for example)
Command /d /c %SystemRoot%\sysnative\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -NonInteractive -NoProfile -File Remove-AdminOUUsers.ps1 0
Command Timeout 1 minute
Download Timeout 10 minutes
Files Upload Remove-AdminOUUsers.ps1InAdminOU.txt is injected automatically by Get-AdminOU.ps1

Note the Package ID (visible in the URL when editing the package) and set $Package2Id in Get-AdminOU.ps1 accordingly.

Log Locations

Log Location
AD query + upload Same directory as Get-AdminOU.ps1Get-AdminOU.log
Endpoint removal C:\ProgramData\Tanium\Remove-AdminOUUsers.log on each endpoint

Sample endpoint log

[2026-05-28 18:57:52] [INFO] [ENDPOINT01] [Mode=0] Starting - InAdminOU.txt: C:\...\InAdminOU.txt
[2026-05-28 18:57:52] [INFO] [ENDPOINT01] [Mode=0] Parsed 19 accounts from InAdminOU.txt
[2026-05-28 18:57:52] [INFO] [ENDPOINT01] [Mode=0] Current local Administrators (3 members): Administrator, jsmith, bjones
[2026-05-28 18:57:52] [INFO] [ENDPOINT01] [Mode=0] Removed DOMAIN\jsmith from local Administrators
[2026-05-28 18:57:52] [INFO] [ENDPOINT01] [Mode=0] Removed DOMAIN\bjones from local Administrators
[2026-05-28 18:57:52] [INFO] [ENDPOINT01] [Mode=0] --- Summary ---
[2026-05-28 18:57:52] [INFO] [ENDPOINT01] [Mode=0] Removed   : 2 - jsmith, bjones
[2026-05-28 18:57:52] [INFO] [ENDPOINT01] [Mode=0] Not found : 17 - ...
[2026-05-28 18:57:52] [INFO] [ENDPOINT01] [Mode=0] Skipped   : 0 -
[2026-05-28 18:57:52] [INFO] [ENDPOINT01] [Mode=0] Enforcement complete

Troubleshooting

Error Cause Fix
Cannot bind argument to parameter 'Path' because it is null Script run from unsaved ISE buffer Save the .ps1 file to disk before running
ScriptRequiresMissingModules: ActiveDirectory RSAT not installed Run Add-WindowsCapability -Online -Name Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0
Could not establish trust relationship for SSL/TLS Connecting to IP instead of hostname, or untrusted cert Use the FQDN (https://tanium.yourdomain.com) and set $BypassSslErrors = $true for lab
{"text":"Forbidden"} on login Wrong username or password, or account lacks API permissions Re-create .bin file; verify user has API role in Tanium console
InAdminOU.txt not found on endpoint Package 2 deployed before Get-AdminOU.ps1 ran Run Package 1 first, confirm upload success, then deploy Package 2
Removal would leave 0 local Administrators All local admins are in the OU list Review InAdminOU.txt — ensure at least one permanent admin is excluded