TL;DR
- With Polycrate, you create a single
inventory.ymlin the workspace root to centrally manage all Linux servers—without needing your own Ansible setup on your laptop. - Each workspace should use its own SSH key pair (not your personal
~/.ssh/key): Polycrate creates it when you runworkspace initwith--with-ssh-keys, stores it underartifacts/secrets/, and wires Ansible so the private key is used withoutansible_ssh_private_key_filein the inventory—unlike plain Ansible. - A basic playbook handles package updates, service checks, and log rotation; a dedicated hardening block takes care of SSH hardening, firewall (ufw), and fail2ban—idempotent and executable as often as needed.
- Compared to
ansible-playbook -i inventory.yml hardening.yml, with Polycrate apolycrate run linux-baseline hardeningsuffices—inclusive of container toolchain and centralinventory.yml(plain YAML, no Jinja2 in the inventory; see Ansible integration). - ayedo supports you with Polycrate, Platform Engineering expertise, and workshops on Polycrate and automation (for example Polycrate Essentials) to set up your Linux server management in a structured and repeatable manner.
Starting Point: Many Linux Servers, Little Time
A typical day as a Linux admin: dozens (or hundreds) of Ubuntu servers, security updates looming, requests from departments, logs filling up—and you actually need a new script for the next special case.
Ansible helps tremendously, but in practice, you probably know these stumbling blocks:
- Different Python/Ansible versions on admin workstations
- Inventories scattered across numerous repos and directories
- SSH keys distributed locally on laptops
- Playbook sprawl without clear structure
Polycrate addresses exactly this: It packages Ansible in a container, brings a complete, reproducible toolchain, and provides a fixed structure with workspaces and blocks. Locally, you only need Polycrate—no Ansible, no Python chaos, no global ansible.cfg experiments.
In this post, we build a small but practical setup:
- A workspace with a central
inventory.yml - SSH: a dedicated key pair per workspace (
workspace init/--with-ssh-keys), not personal keys from~/.ssh/ - A basic playbook for updates, service checks, and log rotation
- A hardening block for SSH, ufw, and fail2ban
- Execution against multiple server groups—idempotent and repeatable
Creating a Workspace and Centralizing Inventory
The entry point in Polycrate is the workspace. It bundles:
workspace.poly– configuration of the workspaceinventory.yml– the central Ansible inventory for all blocksblocks/– your Ansible blocks (includingblock.polyand playbooks)artifacts/secrets/– including SSH keys generated for the workspace (not copies from~/.ssh/)
Workspace Basics
Create a directory and initialize the workspace with the CLI—you do not need to hand-write workspace.poly or SSH keys. See Initialize workspace and SSH keys (best practices).
Polycrate uses $HOME/.polycrate/workspaces/ as the default location for workspaces. Recommended (not mandatory): a folder hierarchy $HOME/.polycrate/workspaces/<organization>/<workspace>/ where the segments match organization and name in workspace.poly, which keeps workspaces cleanly separated (see Best practices – directory layout).
mkdir -p "$HOME/.polycrate/workspaces/acme/acme-corp-automation"
cd "$HOME/.polycrate/workspaces/acme/acme-corp-automation"
polycrate workspace init --with-name acme-corp-automation --with-organization acme --with-ssh-keys
This creates workspace.poly and, with --with-ssh-keys, a workspace-specific key pair under artifacts/secrets/. Then add the block to workspace.poly for this walkthrough:
# workspace.poly (snippet – add blocks)
blocks:
- name: linux-baseline
from: cargo.ayedo.cloud/ayedo/infra/linux-baseline:0.3.1
config:
default_target_group: "linux_all"
nameandorganizationare already set byworkspace init; underblocksyou declare instances. The instance is referenced byname:(e.g.polycrate run linux-baseline …); by convention,namematches the directory name underblocks/(the path mirrors the OCI reference).- The block is published under
cargo.ayedo.cloud. You do not need to runpolycrate blocks pullfirst: if the block is missing locally, Polycrate detects that when you runpolycrate run …and asks whether to install the block from the registry automatically. After that, the unpacked tree lives underblocks/cargo.ayedo.cloud/ayedo/infra/linux-baseline/. Infrom:use the full registry reference including the version tag (OCI sharing instead of a short template name; see Inheritance).
Central Inventory: One Truth for All Blocks
The inventory is always in the workspace root as inventory.yml. Polycrate automatically sets the environment variable ANSIBLE_INVENTORY; you no longer need to specify a -i parameter.
Example:
# inventory.yml
all:
children:
linux_all:
hosts:
srv01.acme-corp.com:
ansible_user: ubuntu
srv02.acme-corp.com:
ansible_user: ubuntu
webservers:
hosts:
srv01.acme-corp.com:
dbservers:
hosts:
srv02.acme-corp.com:
Key points:
linux_allis our default group for all Linux servers.- Subgroups like
webserversanddbserversallow targeted actions. inventory.ymlis static YAML—no Jinja2/templating (neither{{ … }}norworkspace.secrets[…]). In Polycrate, Jinja2 applies to Ansible playbooks and block templates, not the inventory file; see Configuration and Ansible integration – inventory.- You do not need to set the workspace’s SSH private key via
ansible_ssh_private_key_file: Polycrate configures Ansible to use the workspace key automatically—a practical difference from plain Ansible.
With plain Ansible you often maintain per-admin key paths in the inventory or ansible.cfg. With Polycrate the inventory stays lean and key handling is integrated into the runtime.
Secure SSH Keys in the Workspace
SSH is the standard way to reach Linux servers with Ansible. In many teams, private keys are on laptops or USB sticks—suboptimal from a compliance and security perspective.
Polycrate takes a different approach:
- One key pair per workspace—not your personal private key from
~/.ssh/. That keeps automation identity separate from user identity (see Best practices – SSH keys). - Keys are created by
polycrate workspace init --with-ssh-keysand stored underartifacts/secrets/(includingid_rsa,id_rsa.pub). - Polycrate exposes them in the container and wires Ansible automatically to the workspace key—you do not put
ansible_ssh_private_key_fileininventory.yml. Paths to secrets are available in playbooks viaworkspace.secrets[...]when you must reference files explicitly (not in the inventory).
Install the public key on target servers
Add this workspace’s public key (artifacts/secrets/id_rsa.pub) to authorized_keys for the target user on each host, like any dedicated automation key. Do not copy your personal private key from ~/.ssh/id_rsa (or similar) into the workspace.
If a legacy workspace has no keys yet, use polycrate create ssh-keys—do not copy personal private keys into artifacts/secrets/.
Details: SSH in Polycrate.
Basic Playbook: Update Packages, Check Services, Rotate Logs
Now to the actual daily business: keeping systems up to date and checking if everything is running.
We create a block linux-baseline that contains a basic playbook for:
- Package updates (Ubuntu servers)
- Service status checks
- Log rotation (ensuring
logrotateis properly configured and activated)
Block Structure
Create the block folder:
mkdir -p blocks/linux-baseline
cd blocks/linux-baseline
Create a block.poly:
# blocks/linux-baseline/block.poly
name: linux-baseline
version: 0.1.0
kind: generic
config:
default_target_group: "linux_all"
services_to_check:
- ssh
- cron
actions:
- name: base
playbook: base.yml
description: "Update packages, check services, ensure log rotation"
- name: hardening
playbook: hardening.yml
description: "Configure SSH hardening, UFW, and fail2ban"
configdefines block defaults that you can override in the workspace if needed.actionsare named entry points—you don't have to remember complicated Ansible CLI calls.- This block is local—you could later push it to a registry and reuse it in other workspaces (keyword: Sharable Automation via OCI Registry and PolyHub).
The general block syntax is described in the documentation on Blocks.
Basic Playbook base.yml
Now the Ansible playbook that is executed as action base:
# blocks/linux-baseline/base.yml
- name: Linux Basic Management
hosts: "{{ block.config.default_target_group }}"
become: true
gather_facts: true
vars:
logrotate_package: logrotate
tasks:
- name: Update package index
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
- name: Install security updates
ansible.builtin.apt:
upgrade: dist
register: upgrade_result
- name: Show changed packages
ansible.builtin.debug:
var: upgrade_result
when: upgrade_result is changed
- name: Ensure logrotate package
ansible.builtin.apt:
name: "{{ logrotate_package }}"
state: present
- name: Ensure logrotate service (cron.daily)
ansible.builtin.stat:
path: /etc/cron.daily/logrotate
register: logrotate_cron
- name: Create logrotate cron job if not present
ansible.builtin.copy:
dest: /etc/cron.daily/logrotate
content: |
#!/bin/sh
/usr/sbin/logrotate /etc/logrotate.conf
owner: root
group: root
mode: "0755"
when: not logrotate_cron.stat.exists
- name: Check important services
ansible.builtin.service_facts:
- name: Output status of configured services
ansible.builtin.debug:
msg: "Service {{ item }} is {{ ansible_facts.services[item + '.service'].state | default('unknown') }}"
loop: "{{ block.config.services_to_check }}"
when: "item + '.service' in ansible_facts.services"
Key features:
hostsuses the target group configured in the block or workspace (linux_all).- All tasks are idempotent:
aptwithstate: presentorupgradeis repeatable.copyonly writes if the file is missing or changes.- You can run the playbook as often as you like without risking breaking anything—a core advantage of Ansible.
You can execute the whole thing with:
cd "$HOME/.polycrate/workspaces/acme/acme-corp-automation"
polycrate run linux-baseline base
Polycrate starts a container with Ansible and the complete toolchain. You don't need Ansible installed locally; this avoids the usual dependency problem (different Python/Ansible versions, system-wide packages, etc.). See also the Ansible Integration of Polycrate.
Hardening as a Separate Block Action: SSH, UFW, fail2ban
Now it gets security-relevant: securing SSH, activating the firewall, configuring fail2ban. We use the same block definition (linux-baseline), but a second action hardening with its own playbook hardening.yml.
Hardening Playbook hardening.yml
# blocks/linux-baseline/hardening.yml
- name: Linux Hardening
hosts: "{{ block.config.default_target_group }}"
become: true
gather_facts: true
vars:
sshd_config_path: /etc/ssh/sshd_config
allowed_ssh_users:
- ubuntu
ufw_allowed_ports:
- "22/tcp"
- "80/tcp"
- "443/tcp"
tasks:
- name: Disable root login via SSH
ansible.builtin.lineinfile:
path: "{{ sshd_config_path }}"
regexp: "^PermitRootLogin"
line: "PermitRootLogin no"
state: present
create: false
backup: yes
- name: Disable password authentication via SSH
ansible.builtin.lineinfile:
path: "{{ sshd_config_path }}"
regexp: "^PasswordAuthentication"
line: "PasswordAuthentication no"
state: present
create: false
backup: yes
- name: Set allowed SSH users
ansible.builtin.lineinfile:
path: "{{ sshd_config_path }}"
regexp: "^AllowUsers"
line: "AllowUsers {{ allowed_ssh_users | join(' ') }}"
state: present
create: false
backup: yes
- name: Restart SSH service
ansible.builtin.service:
name: ssh
state: restarted
- name: Install UFW
ansible.builtin.apt:
name: ufw
state: present
update_cache: true
- name: "Default policy: incoming deny, outgoing allow"
community.general.ufw:
direction: incoming
policy: deny
- name: Allow outgoing traffic
community.general.ufw:
direction: outgoing
policy: allow
- name: Allow required ports
community.general.ufw:
rule: allow
port: "{{ item }}"
loop: "{{ ufw_allowed_ports }}"
- name: Enable UFW
community.general.ufw:
state: enabled
- name: Install fail2ban
ansible.builtin.apt:
name: fail2ban
state: present
- name: Set basic fail2ban configuration
ansible.builtin.copy:
dest: /etc/fail2ban/jail.local
content: |
[sshd]
enabled = true
filter = sshd
action = iptables[name=SSH, port=ssh, protocol=tcp]
logpath = /var/log/auth.log
maxretry = 5
owner: root
group: root
mode: "0644"
- name: Restart and enable fail2ban service
ansible.builtin.service:
name: fail2ban
state: restarted
enabled: true
Notes:
- UFW uses the
community.general.ufwmodule (via Ansible Galaxy). - All
sshd_configchanges vialineinfileare idempotent. - fail2ban is configured repeatably—re-running applies defined content without uncontrolled drift.
Run against all Linux servers:
cd "$HOME/.polycrate/workspaces/acme/acme-corp-automation"
polycrate run linux-baseline hardening
To harden only web servers, override the block config in the workspace:
# workspace.poly (adjusted snippet)
name: acme-corp-automation
organization: acme
blocks:
- name: linux-baseline
from: cargo.ayedo.cloud/ayedo/infra/linux-baseline:0.3.1
config:
default_target_group: "webservers"
Both base and hardening then target the webservers group from the central inventory—without changing the Ansible playbooks.
Polycrate vs. plain Ansible in daily use
What would this look like without Polycrate?
With plain Ansible you would typically run:
# Base playbook
ansible-playbook -i inventory.yml base.yml
# Hardening
ansible-playbook -i inventory.yml hardening.yml
On your workstation you would also need:
- A matching Ansible version (often via
pip,apt,venv, …) - Python in a compatible version
- A properly configured
ansible.cfg(e.g. for SSH args, inventory path) - Secure handling of your SSH key on the local system
When a colleague needs the same setup, the cycle repeats: align versions, adjust paths, possibly another OS on their laptop.
With Polycrate this reduces to:
polycrate run linux-baseline base
polycrate run linux-baseline hardening
Benefits for you as a Linux admin:
- No local Ansible install—Polycrate always runs Ansible in a container and avoids the classic dependency problem.
- One central
inventory.ymland a clear block model reduce playbook sprawl. - Workspace contents (including secrets) can be protected for idle state and team sharing—see Encrypting the workspace (after setup) at the end of this post.
- What you build once as a block can be versioned, pushed to a registry, and reused elsewhere—internally or via PolyHub, in the spirit of sharable automation.
Polycrate brings Platform Engineering concepts into day-to-day admin work without forcing you to learn Kubernetes or complex CI/CD first.
Frequently asked questions
Do I still need Ansible installed locally if I use Polycrate?
No. Polycrate always runs Ansible inside a container. The required binaries (Ansible, Python, additional tools) are part of the container image you can customize via Dockerfile.poly or a shell script. That avoids typical version conflicts between admin workstations. See the documentation on Ansible integration in Polycrate.
How secure are my SSH keys in the workspace?
Your SSH keys live under artifacts/secrets/. Optionally you can encrypt the entire workspace with age—when to do that and what it means for polycrate run is covered in Encrypting the workspace (after setup) at the end of this post. Basics: SSH in Polycrate.
Can I reuse my existing Ansible playbooks?
Yes. In most cases you can drop existing playbooks almost unchanged as actions in a block—as we did with base.yml and hardening.yml. Use the central inventory.yml in the workspace and avoid local inventories or hosts: localhost with connection: local for SSH-based server management. Polycrate handles the container environment, SSH, secrets, and structured execution via blocks and actions.
More questions? See our FAQ.
Putting it into practice
With this setup you have a solid foundation:
- One central workspace with a single
inventory.ymlfor all Linux servers - Clean SSH key handling via
artifacts/secrets/and Polycrate’s automatic Ansible integration (no key path in the inventory) - A base playbook for updates, service checks, and log rotation
- A hardening playbook for SSH, UFW, and fail2ban, exposed as actions on the
linux-baselineblock - Simple, repeatable execution via
polycrate run, which starts Ansible in a container—without local dependencies
You have moved from ad-hoc scripts and scattered playbooks toward reproducible, structured automation. The same ideas extend easily: more blocks for backup, monitoring agents, compliance scans, or integration with existing tools and processes.
ayedo helps teams make exactly this transition—from manual server care to a thoughtful automation and workspace structure that fits your processes, security requirements, and operations—whether you manage a few dozen Linux servers or a larger estate together with a central Platform Engineering team.
If you want to move your environment to Polycrate and Ansible in a structured way, evaluate concrete playbooks, or design a shared baseline, our server automation workshop is a good fit: Server automation workshop
Encrypting the workspace (after setup)
After you have blocks, inventory, and polycrate run working, you can protect the workspace for idle storage or handoff using built-in age encryption:
polycrate workspace encrypt
Important: An encrypted workspace is not suitable for day-to-day work with actions: polycrate run … requires an unlocked workspace. Encryption is therefore typically the last step when you are done with setup—or you decrypt explicitly before work (details: Workspace encryption).