ESXi NTP Security Configuration

To further extend the hardening of my VMware ESXi hosts, I have been working on scripted ESXi NTP Security Configuration. I wanted to restrict access to the host’s NTP client to the configured NTP Server IPs. Per default, the host firewall is configured to allow all IPs if a service is enabled.

You can add Allowed IP Addresses for an ESXi Host via vSphere Client, vSphere Web Client, vCLI, or PowerCLI. The ESXi Host Client does currently not offer this ability.

The desired ESXi NTP Security Configuration looks like this in the vSphere Client:

ESXi NTP Security Configuration - Time Configuration
ESXi NTP Security Configuration - Firewall Rule

But as usual, it is not an option for me to do this configuration manually using the vSphere Client. I will show how to set the desired configuration with Ansible and PowerCLI.

NTP Security Configuration with Ansible

All required Ansible Modules to enforce the NTP Security Configuration are available out of the box and are maintained by the community.

The Ansible Playbook to set the desired configuration for all hosts in the vSphere Cluster is available on GitHub: vmware_secure_ntp_cluster.yml

NTP Security Configuration with Ansible
- name: Secure ESXi Host NTP Service and enforce NTP Servers
  hosts: localhost
  gather_facts: false
  strategy: free
  vars:
    cluster_name: "cluster01" 
    host_services_query: "host_service_info.*[?key=='ntpd']"
    host_firewall_query: "hosts_firewall_info.*[?key=='ntpClient']"
  tasks:
    - name: Get Services
      vmware_host_service_info:
        hostname: "{{ vcenter_hostname }}"
        username: "{{ vcenter_username }}"
        password: "{{ vcenter_password }}"
        cluster_name: "{{ cluster_name }}"
        validate_certs: no
      register: host_services
      tags: debug
    - name: Debug NTP Service
      debug:
        var: host_services | json_query(host_services_query)
      tags: debug
    - name: Get Firewall Rules
      vmware_host_firewall_info:
        hostname: "{{ vcenter_hostname }}"
        username: "{{ vcenter_username }}"
        password: "{{ vcenter_password }}"
        cluster_name: "{{ cluster_name }}"
        validate_certs: no
      register: host_firewall
      tags: debug
    - name: Debug Firewall Rules
      debug:
        var: host_firewall | json_query(host_firewall_query)
      tags: debug
    - name: Get NTP Config
      vmware_host_ntp_info:
        hostname: "{{ vcenter_hostname }}"
        username: "{{ vcenter_username }}"
        password: "{{ vcenter_password }}"
        cluster_name: "{{ cluster_name }}"
        validate_certs: no
      register: host_ntp
      tags: debug
    - name: Debug NTP Config
      debug:
        var: host_ntp.hosts_ntp_info
      tags: debug
    - name: Manage NTP Firewall Rules
      vmware_host_firewall_manager:
        hostname: "{{ vcenter_hostname }}"
        username: "{{ vcenter_username }}"
        password: "{{ vcenter_password }}"
        cluster_name: "{{ cluster_name }}"  
        validate_certs: no
        rules:
        - name: ntpClient
          enabled: True
          allowed_hosts:
            all_ip: False
            ip_address:
            - 192.168.20.10
            - 192.168.20.11
      tags: ntp  
    - name: Set NTP servers
      vmware_host_ntp:
        hostname: "{{ vcenter_hostname }}"
        username: "{{ vcenter_username }}"
        password: "{{ vcenter_password }}"
        cluster_name: "{{ cluster_name }}"
        validate_certs: no  
        ntp_servers:
        - 192.168.20.10
        - 192.168.20.11
      tags: ntp 
    - name: Set NTP Service
      vmware_host_service_manager:
        hostname: "{{ vcenter_hostname }}"
        username: "{{ vcenter_username }}"
        password: "{{ vcenter_password }}"
        cluster_name: "{{ cluster_name }}"
        validate_certs: no
        service_name: ntpd
        state: start
        service_policy: on 
      tags: ntp  

NTP Security Configuration with PowerCLI

VMware PowerCLI does currently not offer a native Cmdlet to configure allowed IPs or ranges. If you are also interested in this function, please vote for this idea: Cmdlets to manage VMHost Firewall

Even if no native PowerCLI Cmdlet is available, but the required functionality is available through esxcli, Get-ESXCLI can solve the problem.

The PowerCLI Function to set the desired configuration for one or more ESXi hosts is published on the VMware PowerCLI Example Scripts GitHub Repository: Set-VMHostSecureNTP.ps1

NTP Security Configuration with PowerCLI
function Set-VMHostSecureNTP {
<#	
    .NOTES
    ===========================================================================
    Created by: Markus Kraus
    ===========================================================================
    Changelog:  
    2020.05 ver 1.0 Base Release  
    ===========================================================================
    External Code Sources: 
    -
    ===========================================================================
    Tested Against Environment:
    vSphere Version: vSphere 6.7 U3
    PowerCLI Version: PowerCLI 11.5
    PowerShell Version: 5.1
    OS Version: Windows 10
    Keyword: ESXi, NTP, Hardening, Security, Firewall 
    ===========================================================================

    .DESCRIPTION
    This function sets new NTP Servers on given ESXi Hosts and configures the host firewall to only accept NTP connections from these servers.

    .Example
    Get-VMHost | Set-VMHostSecureNTP -Secure

    .Example
    Get-VMHost | Set-VMHostSecureNTP -Type SetSecure -NTP 10.100.1.1, 192.168.2.1

    .PARAMETER VMHost
    Specifies the hosts to configure

    .PARAMETER SetSecure
    Execute Set and Secure operation for new NTP Servers

    .PARAMETER NTP
    Specifies a Array of NTP Servers

    .PARAMETER Secure
    Execute Secure operation for exitsting NTP Servers

#Requires PS -Version 5.1
#Requires -Modules VMware.VimAutomation.Core, @{ModuleName="VMware.VimAutomation.Core";ModuleVersion="11.5.0.0"}
#>

    [CmdletBinding()]
    param( 
        [Parameter(Mandatory=$True, ValueFromPipeline=$True, Position=0, HelpMessage = "Specifies the hosts to configure.")]
            [ValidateNotNullorEmpty()]
            [VMware.VimAutomation.Types.VMHost[]] $VMHost,
        [Parameter(Mandatory=$False, ValueFromPipeline=$False, ParameterSetName="SetSecure", HelpMessage = "Execute Set and Secure operation for new NTP Servers")]
            [Switch] $SetSecure,
        [Parameter(Mandatory=$True, ValueFromPipeline=$False,  ParameterSetName="SetSecure", HelpMessage = "Specifies a Array of NTP Servers")]
            [ValidateNotNullorEmpty()] 
            [ipaddress[]] $NTP,
        [Parameter(Mandatory=$False, ValueFromPipeline=$False, ParameterSetName="Secure", HelpMessage = "Execute Secure operation for exitsting NTP Servers")]
            [Switch] $Secure

    )
    
    begin {

        function SetNTP ($MyHost) {
            ## Get NTP Service 
            "Get NTP Service from VMHost ..." 
            $NTPService = $MyHost | Get-VMHostService | Where-Object {$_.key -eq "ntpd"}  
            ## Stop NTP Service if running   
            "Stop NTP Service if running  ..."        
            if($NTPService.Running -eq $True){
                Stop-VMHostService -HostService $NTPService -Confirm:$false | Out-Null
            }
            ## Enable NTP Service
            "Enable NTP Service if disabled..."
            if($NTPService.Policy -ne "on"){
                Set-VMHostService -HostService $NTPService -Policy "on" -confirm:$False | Out-Null
            }
            ## Remove all existing NTP Servers
            "Remove all existing NTP Servers ..."
            try {
                $MyHost | Get-VMHostNtpServer | Foreach-Object {
                    Remove-VMHostNtpServer -VMHost $MyHost -NtpServer $_ -Confirm:$false
                }
            }
            catch [System.Exception] {
                Write-Warning "Error during removing existing NTP Servers on Host '$($MyHost.Name)'."
            }
            ## Set New NTP Servers
            "Set New NTP Servers ..."
            foreach ($myNTP in $NTP) {
                $MyHost | Add-VMHostNtpServer -ntpserver $myNTP -confirm:$False | Out-Null
            }
            ## Set Current time on Host
            "Set Current time on VMHost ..."
            $HostTimeSystem = Get-View $MyHost.ExtensionData.ConfigManager.DateTimeSystem
            $HostTimeSystem.UpdateDateTime([DateTime]::UtcNow)
            ## Start NTP Service
            "Start NTP Service ..."
            Start-VMHostService -HostService $NTPService -confirm:$False | Out-Null
            ## Get New NTP Servers
            "Get New NTP Servers ..."
            $NewNTPServers = $MyHost | Get-VMHostNtpServer
            "`tNew NTP Servers: $($NewNTPServers -join ", ")"    

        }

        function SecureNTP ($MyHost) {
            ## Get NTP Servers
            "Get NTP Servers to Secure ..."
            [Array]$CurrentNTPServers = $MyHost | Get-VMHostNtpServer
            "`tNTP Servers: $($CurrentNTPServers -join ", ")"
            ## Get ESXCLI -V2
            $esxcli = Get-ESXCLI -VMHost $MyHost -v2
            ## Get NTP Client Firewall
            "Get NTP Client Firewall ..."
            try {
                $FirewallGet = $esxcli.network.firewall.get.Invoke()
            }
            catch [System.Exception]  {
                Write-Warning "Error during Rule List. See latest errors..."
            }
            "`tLoded: $($FirewallGet.Loaded)"
            "`tEnabled: $($FirewallGet.Enabled)"
            "`tDefaultAction: $($FirewallGet.DefaultAction)"
            ## Get NTP Client Firewall Rule
            "Get NTP Client Firewall RuleSet ..."
            $esxcliargs = $esxcli.network.firewall.ruleset.list.CreateArgs()
            $esxcliargs.rulesetid = "ntpClient"
            try {
                $FirewallRuleList = $esxcli.network.firewall.ruleset.list.Invoke($esxcliargs)
            }
            catch [System.Exception]  {
                Write-Warning "Error during Rule List. See latest errors..."
            }
            "`tEnabled: $($FirewallRuleList.Enabled)"
            ## Set NTP Client Firewall Rule
            "Set NTP Client Firewall Rule ..."
            $esxcliargs = $esxcli.network.firewall.ruleset.set.CreateArgs()
            $esxcliargs.enabled = "true" 
            $esxcliargs.allowedall = "false"
            $esxcliargs.rulesetid = "ntpClient"
            try {
                $esxcli.network.firewall.ruleset.set.Invoke($esxcliargs)
            }
            catch [System.Exception]  {
                $ErrorMessage = $_.Exception.Message
                if ($ErrorMessage -ne "Already use allowed ip list") {
                    Write-Warning "Error during Rule Set. See latest errors..."

                }

            }
            "Get NTP Client Firewall Rule AllowedIP ..."
            $esxcliargs = $esxcli.network.firewall.ruleset.allowedip.list.CreateArgs()
            $esxcliargs.rulesetid = "ntpClient"
            try {
                $FirewallRuleAllowedIPList = $esxcli.network.firewall.ruleset.allowedip.list.Invoke($esxcliargs)
            }
            catch [System.Exception]  {
                Write-Warning "Error during Rule List. See latest errors..."
            }
            "`tAllowed IP Addresses: $($FirewallRuleAllowedIPList.AllowedIPAddresses -join ", ")"    
            ## Remove Existing IP from firewall rule
            "Remove Existing IP from firewall rule ..."
            if ($FirewallRuleAllowedIPList.AllowedIPAddresses -ne "All") {
                foreach ($IP in $FirewallRuleAllowedIPList.AllowedIPAddresses) {
                    $esxcliargs = $esxcli.network.firewall.ruleset.allowedip.remove.CreateArgs()
                    $esxcliargs.rulesetid = "ntpClient"
                    $esxcliargs.ipaddress = $IP
                    try {
                        $esxcli.network.firewall.ruleset.allowedip.remove.Invoke($esxcliargs)
                    }
                    catch [System.Exception]  {
                        Write-Warning "Error during AllowedIP remove. See latest errors..."
                    }
                }
                
            }
            ## Set NTP Client Firewall Rule AllowedIP
            "Set NTP Client Firewall Rule AllowedIP ..."
            foreach ($myNTP in $CurrentNTPServers) {
                $esxcliargs = $esxcli.network.firewall.ruleset.allowedip.add.CreateArgs()
                $esxcliargs.ipaddress = $myNTP
                $esxcliargs.rulesetid = "ntpClient"
                try {
                    $esxcli.network.firewall.ruleset.allowedip.add.Invoke($esxcliargs)
                }
                catch [System.Exception]  {
                    $ErrorMessage = $_.Exception.Message
                    if ($ErrorMessage -ne "Ip address already exist.") {
                        Write-Warning "Error during AllowedIP remove. See latest errors..."
                    }
                }             
            }
            ## Get New NTP Client Firewall Rule AllowedIP
            "Get New NTP Client Firewall Rule AllowedIP ..."
            $esxcliargs = $esxcli.network.firewall.ruleset.allowedip.list.CreateArgs()
            $esxcliargs.rulesetid = "ntpClient"
            try {
                $FirewallRuleAllowedIPList = $esxcli.network.firewall.ruleset.allowedip.list.Invoke($esxcliargs)
            }
            catch [System.Exception]  {
                Write-Warning "Error during Rule List. See latest errors..."
            }
            "`tNew Allowed IP Addresses: $($FirewallRuleAllowedIPList.AllowedIPAddresses -join ", ")"    
            
            
        }
        
    }
    
    process {
        
        if ($SetSecure) {
            "Execute Set and Secure operation for new NTP Servers ..."
            $VMHost | Foreach-Object { Write-Output (SetNTP $_) }
            $VMHost | Foreach-Object { Write-Output (SecureNTP $_) }
        }
        if ($Secure) {
            "Execute Secure operation for exitsting NTP Servers ..."
            $VMHost | Foreach-Object { Write-Output (SecureNTP $_) }
        }
        
    }

}

Summary

Ansible was surprisingly well suited to perform this configuration. The vmware_host_firewall_manager Module offered way more possibilities than the VMware PowerCLI. Even when the workaround with Get-ESXCLI works pretty robust, it is still a workaround.

Both examples, the Ansible Playbook, and the PowerCLI Function can be used as a template for even more scripted VMware ESXi security configuration.

I have also merged the ESXi NTP Security Configuration tasks into my existing Ansible Playbook for the additional hardening of VMware ESXi hosts. See also my previous blog post about that topic: ESXi Security Configuration with Ansible

No Responses

Leave a Reply