FreeBSD.software
Home/Guides/How to Manage FreeBSD Servers with Ansible
tutorial·2026-03-29·19 min read

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:

shell
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:

shell
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:

shell
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 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, 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 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.

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 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.

Get more FreeBSD guides

Weekly tutorials, security advisories, and package updates. No spam.