FreeBSD.software
Home/Blog/How to Manage FreeBSD Servers with Ansible
tutorial2026-03-29

How to Manage FreeBSD Servers with Ansible

Complete guide to managing FreeBSD servers with Ansible. Covers installation, inventory, connection setup, playbooks, FreeBSD-specific modules, roles, and practical automation examples.

# How to Manage FreeBSD Servers with Ansible

Managing one FreeBSD server by hand is straightforward. Managing ten is tedious. Managing fifty without automation is a recipe for configuration drift, missed patches, and late-night firefighting. Ansible solves this problem cleanly, and it works with FreeBSD out of the box -- no agent installation required on your target hosts.

This guide covers everything from initial setup to production-ready playbooks for hardening, web server deployment, and jail management. Every example is copy-paste ready and tested against FreeBSD 14.x.

Why Ansible for FreeBSD

Ansible stands apart from other configuration management tools for several reasons that matter specifically to FreeBSD administrators.

**Agentless architecture.** Ansible connects to target hosts over SSH and executes tasks remotely. There is no daemon to install, no ports to open beyond SSH, and no package to keep updated on every managed server. This is a significant advantage on FreeBSD, where you want to keep your attack surface minimal and your base system clean.

**SSH-native.** FreeBSD ships with OpenSSH in the base system. Ansible uses this existing SSH infrastructure directly. If you can SSH into a server, Ansible can manage it. No additional transport layer, no proprietary protocol, no certificates to manage.

**Python-based.** Ansible modules execute Python on the remote host. FreeBSD includes Python in its ports and packages collection, and installing it takes a single pkg install command. Unlike tools that require Ruby or custom runtimes, the Python dependency is lightweight and well-supported on FreeBSD.

**FreeBSD-specific modules.** Ansible ships with modules purpose-built for FreeBSD: community.general.pkgng for package management, community.general.sysrc for rc.conf manipulation, and standard service module support for FreeBSD's rc.d init system. You are not fighting the tool to work around Linux assumptions.

**Idempotent by design.** Every Ansible module is designed to check current state before making changes. Running a playbook twice produces the same result as running it once. This is critical for FreeBSD servers in production where unintended changes cause outages.

Installing Ansible

On the Control Node

The control node is the machine you run Ansible from -- typically your workstation or a dedicated management server. Ansible runs on Linux, macOS, or FreeBSD itself.

On a FreeBSD control node:

bash

pkg install py311-ansible

On a Linux or macOS control node using pip:

bash

python3 -m pip install --user ansible

Verify the installation:

bash

ansible --version

You should see output showing Ansible core 2.16 or later.

On FreeBSD Target Hosts

Target hosts need only two things: SSH access and Python. SSH is already present in the FreeBSD base system. Install Python:

bash

pkg install python311

That is the only package Ansible requires on managed hosts. No agent, no daemon, no service to configure.

Inventory Setup

The inventory file tells Ansible which servers to manage and how to group them. Create a directory structure for your Ansible project:

bash

mkdir -p ~/ansible-freebsd/{inventory,playbooks,roles,group_vars,host_vars}

Create the inventory file at ~/ansible-freebsd/inventory/hosts.yml:

yaml

all:

children:

webservers:

hosts:

web01.example.com:

ansible_host: 203.0.113.10

web02.example.com:

ansible_host: 203.0.113.11

dbservers:

hosts:

db01.example.com:

ansible_host: 203.0.113.20

jailhosts:

hosts:

jail01.example.com:

ansible_host: 203.0.113.30

freebsd:

children:

webservers:

dbservers:

jailhosts:

The freebsd group is a parent group that contains all FreeBSD hosts. This lets you apply FreeBSD-specific variables to every host at once.

Group Variables

Create ~/ansible-freebsd/group_vars/freebsd.yml to set defaults for all FreeBSD hosts:

yaml

ansible_python_interpreter: /usr/local/bin/python3.11

ansible_become_method: su

ansible_become: true

ansible_user: admin

These four variables are critical for FreeBSD. Without them, Ansible will fail on its first connection attempt. The next section explains why.

Connection Configuration

FreeBSD differs from Linux in ways that affect Ansible's default behavior. You must configure these settings or every task will fail.

Python Interpreter Path

Linux distributions install Python at /usr/bin/python3. FreeBSD installs it at /usr/local/bin/python3.11 (or whichever version you installed). Ansible must know where to find Python on the remote host:

yaml

ansible_python_interpreter: /usr/local/bin/python3.11

Set this in your group variables for all FreeBSD hosts. Without it, Ansible will look for Python in the wrong location and every module will fail with a cryptic error.

Become Method

Linux servers typically use sudo for privilege escalation. FreeBSD includes su in the base system but does not include sudo by default. You have two options:

**Option 1: Use su (no extra packages)**

yaml

ansible_become_method: su

ansible_become_pass: "{{ vault_root_password }}"

This requires the root password, which you should store in Ansible Vault (covered later in this guide).

**Option 2: Install and use doas**

bash

pkg install doas

Configure /usr/local/etc/doas.conf:


permit nopass keepenv :wheel

Then set in your group variables:

yaml

ansible_become_method: doas

doas is a simpler alternative to sudo that originated in OpenBSD and is well-supported on FreeBSD. Many FreeBSD administrators prefer it over sudo for its smaller codebase and simpler configuration.

SSH Configuration

Create or update ~/.ssh/config on your control node for cleaner connections:


Host *.example.com

User admin

IdentityFile ~/.ssh/ansible_ed25519

StrictHostKeyChecking accept-new

The ansible.cfg File

Create ~/ansible-freebsd/ansible.cfg:

ini

[defaults]

inventory = inventory/hosts.yml

roles_path = roles

vault_password_file = .vault_pass

host_key_checking = False

retry_files_enabled = False

stdout_callback = yaml

[privilege_escalation]

become = True

become_method = su

become_user = root

[ssh_connection]

pipelining = True

ssh_args = -o ControlMaster=auto -o ControlPersist=60s

Enabling pipelining significantly speeds up Ansible on FreeBSD by reducing the number of SSH operations per task.

First Ad-Hoc Commands

Before writing playbooks, verify that Ansible can reach your FreeBSD hosts with ad-hoc commands.

Ping All Hosts

bash

ansible freebsd -m ping

Expected output:


web01.example.com | SUCCESS => {

"changed": false,

"ping": "pong"

}

If this fails, check your Python interpreter path, SSH key, and become method settings.

Run a Shell Command

bash

ansible freebsd -m shell -a "freebsd-version"

This returns the FreeBSD version running on each host.

Check Installed Packages

bash

ansible webservers -m shell -a "pkg info | wc -l"

Install a Package

bash

ansible webservers -m community.general.pkgng -a "name=htop state=present"

Ad-hoc commands are useful for quick checks and one-off tasks. For repeatable configuration, use playbooks.

Writing Playbooks for FreeBSD

Playbooks are YAML files that describe the desired state of your servers. Here is the anatomy of a FreeBSD-aware playbook.

Package Management with pkgng

The community.general.pkgng module manages FreeBSD packages:

yaml

- name: Install common packages

hosts: freebsd

become: true

tasks:

- name: Ensure pkg is bootstrapped

raw: env ASSUME_ALWAYS_YES=yes pkg bootstrap

changed_when: false

- name: Install base packages

community.general.pkgng:

name:

- vim

- htop

- tmux

- curl

- rsync

- py311-pip

state: present

The raw task for bootstrapping pkg is important. On a fresh FreeBSD install, the pkg tool itself may not be initialized. The raw module executes a command without requiring Python, making it safe to run before anything else.

System Configuration with sysrc

The community.general.sysrc module manipulates /etc/rc.conf, which is the central configuration file for FreeBSD services:

yaml

- name: Configure system settings

hosts: freebsd

become: true

tasks:

- name: Set hostname

community.general.sysrc:

name: hostname

value: "{{ inventory_hostname }}"

- name: Enable sshd

community.general.sysrc:

name: sshd_enable

value: "YES"

- name: Set default gateway

community.general.sysrc:

name: defaultrouter

value: "203.0.113.1"

- name: Enable PF firewall

community.general.sysrc:

name: pf_enable

value: "YES"

This module understands rc.conf syntax and handles quoting, escaping, and merging correctly. Never use lineinfile to edit rc.conf -- it will eventually break your configuration.

Service Management

The built-in service module works with FreeBSD's rc.d system:

yaml

- name: Manage services

hosts: freebsd

become: true

tasks:

- name: Start and enable sshd

service:

name: sshd

state: started

enabled: true

- name: Restart networking

service:

name: netif

state: restarted

Ansible detects FreeBSD automatically and uses the correct service management commands (service(8) and sysrc(8)) behind the scenes.

FreeBSD-Specific Modules and Gotchas

Working with FreeBSD in Ansible requires understanding several key differences from Linux.

No systemd

FreeBSD uses the rc.d init system, not systemd. There are no unit files, no systemctl, no journal. Services are controlled through shell scripts in /usr/local/etc/rc.d/ (for ports/packages) and /etc/rc.d/ (for base system services). The service module handles this transparently, but if you are porting playbooks from Linux, you will need to remove any systemd-specific tasks.

rc.conf Is the Single Source of Truth

On FreeBSD, /etc/rc.conf controls which services start at boot and their configuration. Always use the sysrc module to modify it. Direct file manipulation with lineinfile or template can corrupt the file or create conflicting entries.

pkg vs Ports

The pkgng module manages binary packages installed via pkg. If you need to compile software from the ports tree with custom options, use the shell module:

yaml

- name: Build from ports with custom options

shell: |

cd /usr/ports/www/nginx && \

make OPTIONS_SET="HTTP_GZIP HTTP_SSL HTTP_V2" \

BATCH=yes install clean

args:

creates: /usr/local/sbin/nginx

The creates parameter ensures this task only runs if NGINX is not already installed, maintaining idempotency.

File Paths

FreeBSD installs third-party software under /usr/local/, not /usr/ or /opt/. Configuration files go in /usr/local/etc/, not /etc/. Account for this in all template and copy tasks:

yaml

- name: Deploy nginx configuration

template:

src: nginx.conf.j2

dest: /usr/local/etc/nginx/nginx.conf

owner: root

group: wheel

mode: '0644'

Note that the default group is wheel, not root as on Linux.

User Management

FreeBSD uses pw(8) for user management. The Ansible user module supports FreeBSD, but some parameters behave differently. The default shell is /bin/sh, not /bin/bash, and the skeleton directory is /usr/share/skel.

Practical Playbook: Hardening a FreeBSD Server

This playbook implements essential [FreeBSD hardening](/blog/hardening-freebsd-server/) measures. Save it as playbooks/harden.yml:

yaml

---

- name: Harden FreeBSD servers

hosts: freebsd

become: true

vars:

ssh_port: 22

allowed_users: "admin"

tasks:

- name: Update all packages

community.general.pkgng:

name: "*"

state: latest

- name: Install security tools

community.general.pkgng:

name:

- doas

- lynis

- rkhunter

state: present

- name: Configure doas

copy:

content: |

permit nopass keepenv :wheel

permit nopass keepenv root

dest: /usr/local/etc/doas.conf

owner: root

group: wheel

mode: '0600'

- name: Harden SSH - disable root login

lineinfile:

path: /etc/ssh/sshd_config

regexp: '^#?PermitRootLogin'

line: 'PermitRootLogin no'

notify: restart sshd

- name: Harden SSH - disable password authentication

lineinfile:

path: /etc/ssh/sshd_config

regexp: '^#?PasswordAuthentication'

line: 'PasswordAuthentication no'

notify: restart sshd

- name: Harden SSH - allow only specific users

lineinfile:

path: /etc/ssh/sshd_config

regexp: '^#?AllowUsers'

line: "AllowUsers {{ allowed_users }}"

notify: restart sshd

- name: Harden SSH - disable X11 forwarding

lineinfile:

path: /etc/ssh/sshd_config

regexp: '^#?X11Forwarding'

line: 'X11Forwarding no'

notify: restart sshd

- name: Set secure sysctl values

sysctl:

name: "{{ item.name }}"

value: "{{ item.value }}"

state: present

reload: true

loop:

- { name: 'net.inet.tcp.blackhole', value: '2' }

- { name: 'net.inet.udp.blackhole', value: '1' }

- { name: 'security.bsd.see_other_uids', value: '0' }

- { name: 'security.bsd.see_other_gids', value: '0' }

- { name: 'security.bsd.unprivileged_read_msgbuf', value: '0' }

- { name: 'security.bsd.unprivileged_proc_debug', value: '0' }

- { name: 'kern.randompid', value: '1' }

- name: Enable PF firewall

community.general.sysrc:

name: pf_enable

value: "YES"

- name: Deploy PF firewall rules

template:

src: templates/pf.conf.j2

dest: /etc/pf.conf

owner: root

group: wheel

mode: '0600'

notify: reload pf

- name: Enable and start PF

service:

name: pf

state: started

enabled: true

- name: Set secure console permissions

copy:

content: |

console none unknown off insecure

dest: /etc/ttys.d/console.conf

owner: root

group: wheel

mode: '0644'

when: false # Enable manually after verifying physical access

- name: Clear /tmp on boot

community.general.sysrc:

name: clear_tmp_enable

value: "YES"

- name: Enable syslog TLS (if remote logging configured)

community.general.sysrc:

name: syslogd_flags

value: "-ss"

handlers:

- name: restart sshd

service:

name: sshd

state: restarted

- name: reload pf

shell: pfctl -f /etc/pf.conf

Create the PF template at templates/pf.conf.j2:

jinja2

# Ansible managed - do not edit manually

ext_if = "vtnet0"

set block-policy drop

set skip on lo0

# Scrub incoming packets

scrub in all

# Default deny

block all

# Allow outbound traffic

pass out quick on $ext_if proto { tcp, udp, icmp } from ($ext_if) to any modulate state

# Allow SSH

pass in on $ext_if proto tcp from any to ($ext_if) port {{ ssh_port }} flags S/SA modulate state

# Allow HTTP and HTTPS (webservers only)

{% if 'webservers' in group_names %}

pass in on $ext_if proto tcp from any to ($ext_if) port { 80, 443 } flags S/SA modulate state

{% endif %}

# Allow ICMP ping

pass in on $ext_if inet proto icmp icmp-type echoreq

Run the hardening playbook:

bash

ansible-playbook playbooks/harden.yml

Practical Playbook: Deploying NGINX with Let's Encrypt

This playbook installs and configures NGINX with automatic TLS certificates. For the full [NGINX setup guide on FreeBSD](/blog/nginx-freebsd-production-setup/), see our dedicated tutorial.

Save as playbooks/nginx-letsencrypt.yml:

yaml

---

- name: Deploy NGINX with Let's Encrypt on FreeBSD

hosts: webservers

become: true

vars:

domain: "example.com"

webroot: "/usr/local/www/{{ domain }}"

email: "admin@example.com"

tasks:

- name: Install NGINX and Certbot

community.general.pkgng:

name:

- nginx

- py311-certbot

- py311-certbot-nginx

state: present

- name: Create webroot directory

file:

path: "{{ webroot }}"

state: directory

owner: www

group: www

mode: '0755'

- name: Deploy initial NGINX configuration

template:

src: templates/nginx-initial.conf.j2

dest: /usr/local/etc/nginx/nginx.conf

owner: root

group: wheel

mode: '0644'

notify: reload nginx

- name: Enable and start NGINX

community.general.sysrc:

name: nginx_enable

value: "YES"

- name: Start NGINX

service:

name: nginx

state: started

- name: Obtain Let's Encrypt certificate

command: >

certbot certonly

--webroot

--webroot-path {{ webroot }}

--email {{ email }}

--agree-tos

--no-eff-email

-d {{ domain }}

-d www.{{ domain }}

args:

creates: "/usr/local/etc/letsencrypt/live/{{ domain }}/fullchain.pem"

- name: Deploy production NGINX configuration with TLS

template:

src: templates/nginx-tls.conf.j2

dest: /usr/local/etc/nginx/nginx.conf

owner: root

group: wheel

mode: '0644'

notify: reload nginx

- name: Configure certificate auto-renewal

cron:

name: "certbot renew"

minute: "30"

hour: "3"

weekday: "1"

job: "/usr/local/bin/certbot renew --quiet --deploy-hook 'service nginx reload'"

user: root

- name: Set NGINX performance tuning in rc.conf

community.general.sysrc:

name: nginx_flags

value: ""

handlers:

- name: reload nginx

service:

name: nginx

state: reloaded

Practical Playbook: Managing FreeBSD Jails

[FreeBSD jails](/blog/freebsd-jails-guide/) are lightweight containers native to the operating system. This playbook automates jail creation and management using the bastille jail manager.

Save as playbooks/jails.yml:

yaml

---

- name: Manage FreeBSD jails with Bastille

hosts: jailhosts

become: true

vars:

jail_release: "14.2-RELEASE"

jails:

- name: webjail

ip: "10.0.0.10"

interface: "lo1"

packages:

- nginx

- php83

- name: dbjail

ip: "10.0.0.20"

interface: "lo1"

packages:

- postgresql16-server

- postgresql16-client

tasks:

- name: Install Bastille

community.general.pkgng:

name: bastille

state: present

- name: Enable Bastille at boot

community.general.sysrc:

name: bastille_enable

value: "YES"

- name: Create cloned loopback interface

community.general.sysrc:

name: cloned_interfaces

value: "lo1"

- name: Configure loopback network

community.general.sysrc:

name: ifconfig_lo1_alias0

value: "inet 10.0.0.0/24"

- name: Bring up lo1 interface

command: ifconfig lo1 create

failed_when: false

changed_when: false

- name: Configure lo1 address

command: ifconfig lo1 inet 10.0.0.0/24

failed_when: false

changed_when: false

- name: Bootstrap Bastille release

command: "bastille bootstrap {{ jail_release }} update"

args:

creates: "/usr/local/bastille/releases/{{ jail_release }}/bin/freebsd-version"

- name: Create jails

command: "bastille create {{ item.name }} {{ jail_release }} {{ item.ip }} {{ item.interface }}"

args:

creates: "/usr/local/bastille/jails/{{ item.name }}"

loop: "{{ jails }}"

- name: Install packages in each jail

command: "bastille pkg {{ item.0.name }} install -y {{ item.1 }}"

loop: "{{ jails | subelements('packages') }}"

changed_when: false

- name: Start all jails

command: "bastille start {{ item.name }}"

loop: "{{ jails }}"

changed_when: false

failed_when: false

- name: Add PF rules for jail NAT

blockinfile:

path: /etc/pf.conf

marker: "# {mark} ANSIBLE MANAGED - JAIL NAT"

block: |

nat on vtnet0 from 10.0.0.0/24 to any -> (vtnet0)

pass in on lo1 all

notify: reload pf

handlers:

- name: reload pf

shell: pfctl -f /etc/pf.conf

For a complete guide on jail networking, resource limits, and template management, see our [FreeBSD jails guide](/blog/freebsd-jails-guide/).

Roles and Galaxy Collections for FreeBSD

As your playbooks grow, organize them into reusable roles.

Creating a Role

bash

ansible-galaxy role init roles/freebsd_base

This creates the standard role directory structure. Move your common FreeBSD tasks into roles/freebsd_base/tasks/main.yml:

yaml

---

- name: Bootstrap pkg

raw: env ASSUME_ALWAYS_YES=yes pkg bootstrap

changed_when: false

- name: Update package repository

community.general.pkgng:

name: pkg

state: latest

- name: Install base packages

community.general.pkgng:

name: "{{ freebsd_base_packages }}"

state: present

- name: Set timezone

file:

src: "/usr/share/zoneinfo/{{ freebsd_timezone }}"

dest: /etc/localtime

state: link

force: true

- name: Configure NTP

community.general.sysrc:

name: ntpd_enable

value: "YES"

- name: Start NTP

service:

name: ntpd

state: started

Define defaults in roles/freebsd_base/defaults/main.yml:

yaml

---

freebsd_timezone: "UTC"

freebsd_base_packages:

- vim

- htop

- tmux

- curl

- rsync

- doas

- py311-pip

Galaxy Collections

Install the community.general collection, which contains FreeBSD-specific modules:

bash

ansible-galaxy collection install community.general

Create a requirements.yml file to pin collection versions:

yaml

---

collections:

- name: community.general

version: ">=9.0.0"

Install from the requirements file:

bash

ansible-galaxy collection install -r requirements.yml

Some community roles specifically target FreeBSD. Search Galaxy for them:

bash

ansible-galaxy search freebsd --platforms FreeBSD

Vault for Secrets Management

Never store passwords, API keys, or private data in plain text. Ansible Vault encrypts sensitive variables.

Creating Encrypted Variables

Create an encrypted variables file:

bash

ansible-vault create group_vars/freebsd/vault.yml

Add your secrets:

yaml

vault_root_password: "your-root-password-here"

vault_db_password: "database-password-here"

vault_certbot_email: "real-email@example.com"

Using Vault in Playbooks

Reference vault variables like any other variable:

yaml

- name: Set database password

shell: "echo '{{ vault_db_password }}' | pw usermod postgres -h 0"

no_log: true

The no_log: true directive prevents Ansible from printing the password in its output.

Running Playbooks with Vault

Using a password file (recommended for automation):

bash

echo 'your-vault-password' > .vault_pass

chmod 600 .vault_pass

ansible-playbook playbooks/harden.yml --vault-password-file .vault_pass

Add .vault_pass to your .gitignore immediately. Never commit vault passwords to version control.

Using interactive password prompt:

bash

ansible-playbook playbooks/harden.yml --ask-vault-pass

Testing with Molecule

Molecule provides a framework for testing Ansible roles. Since FreeBSD does not run in Docker containers natively, use Vagrant with VirtualBox or a cloud provider as the driver.

Install Molecule

bash

pip install molecule molecule-vagrant python-vagrant

Initialize Molecule for a Role

bash

cd roles/freebsd_base

molecule init scenario --driver-name vagrant

Edit molecule/default/molecule.yml:

yaml

---

dependency:

name: galaxy

driver:

name: vagrant

provider:

name: virtualbox

platforms:

- name: freebsd-test

box: freebsd/FreeBSD-14.2-RELEASE

memory: 1024

cpus: 1

provisioner:

name: ansible

inventory:

group_vars:

all:

ansible_python_interpreter: /usr/local/bin/python3.11

verifier:

name: ansible

Create verification tests in molecule/default/verify.yml:

yaml

---

- name: Verify FreeBSD base role

hosts: all

become: true

tasks:

- name: Check that base packages are installed

command: pkg info vim

changed_when: false

- name: Check NTP is running

command: service ntpd status

changed_when: false

- name: Verify timezone is set

command: readlink /etc/localtime

register: tz_result

changed_when: false

failed_when: "'UTC' not in tz_result.stdout"

Run the full test cycle:

bash

molecule test

This creates a FreeBSD VM, applies your role, runs verification tests, and destroys the VM. Use molecule converge during development to keep the VM running between iterations.

An alternative to Vagrant is testing against a [FreeBSD VPS](/blog/freebsd-vps-setup/) instance. Cloud providers like Vultr and DigitalOcean offer FreeBSD images that spin up in under a minute, making them practical for CI/CD pipelines.

Putting It All Together

A site-wide playbook that calls your roles in sequence:

yaml

---

- name: Configure all FreeBSD servers

hosts: freebsd

become: true

roles:

- freebsd_base

- freebsd_hardening

- name: Configure web servers

hosts: webservers

become: true

roles:

- freebsd_nginx

- freebsd_certbot

- name: Configure database servers

hosts: dbservers

become: true

roles:

- freebsd_postgresql

- name: Configure jail hosts

hosts: jailhosts

become: true

roles:

- freebsd_jails

Run everything:

bash

ansible-playbook site.yml --vault-password-file .vault_pass

Run only against web servers:

bash

ansible-playbook site.yml --limit webservers

Dry run to see what would change:

bash

ansible-playbook site.yml --check --diff

FAQ

Does Ansible work with FreeBSD out of the box?

Yes. Ansible supports FreeBSD as a target platform. You need Python installed on the FreeBSD host and must set ansible_python_interpreter to the correct path (/usr/local/bin/python3.11). The community.general collection includes FreeBSD-specific modules for package management and rc.conf manipulation. No custom plugins or patches are needed.

What become method should I use on FreeBSD?

FreeBSD does not include sudo in the base system. Your best options are su (requires root password, available by default) or doas (requires installing the doas package). Set ansible_become_method in your group variables. For automated environments, doas with permit nopass keepenv :wheel is the most practical choice because it does not require passing a root password.

Can Ansible manage FreeBSD packages from the ports tree?

The community.general.pkgng module manages binary packages installed via pkg. For ports, use the shell module with make install commands. Set BATCH=yes to avoid interactive prompts, and use the creates parameter to maintain idempotency. Binary packages cover the vast majority of use cases; compiling from ports is only necessary when you need custom build options.

How do I handle FreeBSD updates with Ansible?

For package updates, use community.general.pkgng with state: latest and name: "*" to update all installed packages. For base system updates (freebsd-update), use the command module:

yaml

- name: Fetch and install FreeBSD security patches

command: freebsd-update --not-running-from-cron fetch install

register: update_result

changed_when: "'Installing updates' in update_result.stdout"

Schedule this in a dedicated maintenance playbook and always test on a staging server first.

Can Ansible manage FreeBSD jails?

Yes. While Ansible does not have a dedicated jail module in the core collection, you can manage jails through jail management tools like Bastille, BastilleBSD, or iocage using the command and shell modules. The playbook example in this guide demonstrates this approach with Bastille. For complex jail environments, write a custom Ansible module or use the community-maintained jail roles on Galaxy.

How do I speed up Ansible on FreeBSD?

Enable SSH pipelining in ansible.cfg by setting pipelining = True. This reduces the number of SSH connections per task significantly. Use ControlMaster and ControlPersist in your SSH configuration to reuse connections. Consider using mitogen as a strategy plugin for further speed improvements. Finally, group independent tasks using async and poll for parallel execution within a single host.

Is Ansible suitable for managing hundreds of FreeBSD servers?

Yes. Ansible scales well with proper configuration. Use --forks to control parallelism (default is 5, increase to 20-50 for large fleets). Organize your inventory into logical groups and use --limit for targeted runs. For very large deployments, consider AWX or Ansible Automation Platform for centralized management, scheduling, and audit logging. The agentless architecture means scaling does not require deploying or updating agents across your fleet.

Conclusion

Ansible and FreeBSD are a natural fit. The agentless architecture respects FreeBSD's philosophy of keeping the base system clean. The SSH transport leverages what FreeBSD already provides. The FreeBSD-specific modules in community.general handle the platform's unique characteristics -- rc.conf management, pkgng packages, and rc.d services -- without requiring workarounds.

Start with the hardening playbook from this guide, adapt it to your environment, and build from there. Once your first playbook is working, every additional server you bring under Ansible management reduces your operational burden. Configuration drift disappears. Rebuilding a server becomes a single command. And your documentation is the playbook itself -- always up to date, always executable.