Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion ansible.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ roles_path = ~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles:../rol
timeout = 90
host_key_checking = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=yes
; improving preformances through pipelining - disabled due to race conditions on first install
; pipelining = True
; NOTE: Pipelining is disabled because it can cause "Permission denied" errors
; when a playbook creates a new user and then runs tasks as that user,
; especially if hardening has been applied. This is because pipelining requires
; write access to the user's home directory (~/.ansible/tmp), which may be
; restricted. I also seemed to cause race-conditions in some cases

[galaxy]
url = https://galaxy.ansible.com/
201 changes: 158 additions & 43 deletions install-docker-rootless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,36 +37,25 @@
- name: Install Docker in Rootless Mode
hosts: all
any_errors_fatal: true

# Pre-flight checks to ensure system compatibility
pre_tasks:
- name: Validate kernel support for user namespaces
ansible.builtin.shell: |
if [ -f /proc/sys/user/max_user_namespaces ]; then
max_ns=$(cat /proc/sys/user/max_user_namespaces)
if [ "$max_ns" -gt 0 ]; then
echo "User namespaces supported (max: $max_ns)"
else
echo "User namespaces disabled"
exit 1
fi
else
echo "User namespace support unknown"
fi
register: namespace_check
changed_when: false
failed_when: namespace_check.rc != 0
tags: [validation, always]

- name: Display Docker installation information
ansible.builtin.debug:
msg:
- "Target host: {{ inventory_hostname }}"
- "Docker user: {{ DOCKER_USER | default('dockeruser') }}"
- "Rootless mode: Enabled"
- "Compose installation: {{ DOCKER_COMPOSE | default(false) }}"
- "Privileged ports: {{ DOCKER_ALLOW_PRIVILEGED_PORTS | default(false) }}"
tags: [info, always]
- name: Check if unprivileged_userns AppArmor profile exists
ansible.builtin.stat:
path: /etc/apparmor.d/unprivileged_userns
become: true
register: unprivileged_userns_apparmor_profile
tags: [docker-config, apparmor]

- name: Install pasta (passt) package for docker rootless networking
ansible.builtin.apt:
name: passt
state: present
update_cache: true
become: true
tags: [docker-setup, packages]


tasks:
# ===================================================================
Expand Down Expand Up @@ -95,7 +84,7 @@
ansible.builtin.git:
repo: https://github.com/konstruktoid/ansible-role-docker-rootless.git
dest: "{{ lookup('env', 'HOME') }}/.ansible/roles/konstruktoid.docker_rootless"
version: 'v0.58.0' # Pinned version for reproducibility
version: 'v1.6.0' # Pinned version for reproducibility
delegate_to: localhost
run_once: true
tags: [docker-setup, role-download]
Expand All @@ -116,69 +105,195 @@
docker_allow_privileged_ports: "{{ DOCKER_ALLOW_PRIVILEGED_PORTS | default(false) }}" # Allow binding to ports <1024

# === DOCKER COMPOSE CONFIGURATION ===
docker_compose: "{{ DOCKER_COMPOSE | default(false) }}" # Install Docker Compose
docker_compose_arch: "{{ DOCKER_COMPOSE_ARCH | default('x86_64') }}" # Architecture for Compose download
docker_compose: "{{ DOCKER_COMPOSE | default(true) }}" # Install Docker Compose

# === ROOTFUL DOCKER CONFIGURATION ===
# These options control traditional rootful Docker (usually disabled for security)
docker_rootful_enabled: "{{ DOCKER_ROOTFUL_ENABLED | default(false) }}" # Enable traditional Docker daemon
docker_rootful: "{{ DOCKER_ROOTFUL | default(false) }}" # Install rootful Docker alongside rootless
docker_rootful_opts: "{{ DOCKER_ROOTFUL_OPTS | default(false) }}" # Additional rootful Docker options

# === NETWORK DRIVER CONFIGURATION ===
# pasta: Modern, better AppArmor compatibility, works with hidepid=2
docker_driver_network: "{{ DOCKER_NETWORK_DRIVER | default('pasta') }}"
# implicit port driver required for pasta
docker_driver_port: "implicit"

# === SERVICE MANAGEMENT ===
docker_service_restart: "{{ DOCKER_SERVICE_RESTART | default(true) }}" # Restart Docker service after installation
docker_service_restart: true # Let the role handle service restart properly
docker_unattended_upgrades: "{{ DOCKER_UNATTENDED_UPGRADES | default(true) }}" # Enable automatic Docker updates

# === USER CONFIGURATION ===
create_docker_user: true
docker_user_bashrc: "{{ DOCKER_USER_BASHRC | default(true) }}" # Add Docker environment to user's .bashrc
docker_user: "{{ DOCKER_USER | default('dockeruser') }}" # Username for Docker installation
docker_user: "{{ DOCKER_USER | default('dockeruser') }}" # Username for Docker installation
tags: [docker-installation, docker-config]

# Post-installation validation and information
post_tasks:
- name: Verify Docker rootless installation
ansible.builtin.command: |
sudo -u {{ DOCKER_USER | default('dockeruser') }} docker version

- name: Ensure AppArmor local directory exists
ansible.builtin.file:
path: /etc/apparmor.d/local
state: directory
mode: '0755'
become: true
when: unprivileged_userns_apparmor_profile.stat.exists
tags: [docker-config, apparmor]

- name: Create minimal AppArmor local overrides for unprivileged_userns
ansible.builtin.template:
src: templates/apparmor-local-unprivileged_userns.j2
dest: /etc/apparmor.d/local/unprivileged_userns
mode: '0644'
become: true
when: unprivileged_userns_apparmor_profile.stat.exists
register: unprivileged_userns_local_overrides
tags: [docker-config, apparmor]

- name: Reload unprivileged_userns AppArmor profile
ansible.builtin.command:
cmd: apparmor_parser -r /etc/apparmor.d/unprivileged_userns
become: true
when:
- unprivileged_userns_apparmor_profile.stat.exists
- unprivileged_userns_local_overrides.changed
tags: [docker-config, apparmor]

- name: Configure Docker to use cgroupfs driver (bypasses systemd/polkit issues)
ansible.builtin.copy:
dest: "/home/{{ DOCKER_USER | default('dockeruser') }}/.config/docker/daemon.json"
mode: '0644'
content: |
{
"exec-opts": ["native.cgroupdriver=cgroupfs"]
}
become: true
become_user: "{{ DOCKER_USER | default('dockeruser') }}"
register: docker_daemon_config
tags: [docker-config]

- name: Restart docker service if daemon.json or AppArmor changed
ansible.builtin.shell: |
export XDG_RUNTIME_DIR=/run/user/$(id -u)
systemctl --user daemon-reload
systemctl --user restart docker
args:
executable: /bin/bash
become: true
become_user: "{{ DOCKER_USER | default('dockeruser') }}"
when:
- docker_daemon_config.changed
or (unprivileged_userns_local_overrides is defined and unprivileged_userns_local_overrides.changed)
register: docker_restart_result
retries: 3 # Retry up to 3 times for timing issues
delay: 2 # Wait 2 seconds between retries
until: docker_restart_result.rc == 0
tags: [docker-config, always]

- name: Check Docker version for rootless user
ansible.builtin.shell: "export PATH=$PATH:$HOME/.local/bin && export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock && docker --version"
register: docker_version_check
args:
executable: /bin/bash
become: true
become_user: "{{ DOCKER_USER | default('dockeruser') }}"
changed_when: false
failed_when: false
tags: [validation, post-check]
tags: [info, summary, always]

- name: Check Docker daemon status for user
ansible.builtin.command: |
sudo -u {{ DOCKER_USER | default('dockeruser') }} systemctl --user is-active docker
- name: Check Docker service status for rootless user
ansible.builtin.shell: "export XDG_RUNTIME_DIR=/run/user/$(id -u) && systemctl --user status docker"
register: docker_service_check
args:
executable: /bin/bash
become: true
become_user: "{{ DOCKER_USER | default('dockeruser') }}"
changed_when: false
failed_when: false
tags: [validation, post-check]
tags: [info, summary, always]

- name: Display raw Docker service status for debugging
ansible.builtin.debug:
msg: "Raw Docker service status:\n{{ docker_service_check.stdout }}\n{{ docker_service_check.stderr }}"
tags: [debug, always]

- name: Display Docker installation summary
ansible.builtin.debug:
msg:
- "===== DOCKER ROOTLESS INSTALLATION SUMMARY ====="
- "Host: {{ inventory_hostname }}"
- "Docker user: {{ DOCKER_USER | default('dockeruser') }}"
- "Docker version: {{ docker_version_check.stdout.split('\n')[1] if docker_version_check.rc == 0 else 'Check failed' }}"
- "Docker version: {{ docker_version_check.stdout if docker_version_check.rc == 0 else 'Check failed' }}"
- "Service status: {{ docker_service_check.stdout if docker_service_check.rc == 0 else 'Check failed' }}"
- "Rootless mode: Enabled"
- "Compose installed: {{ DOCKER_COMPOSE | default(false) }}"
- "=================================================="
tags: [info, summary, always]

- name: Get Docker user info for usage instructions
become: true
ansible.builtin.user:
name: "{{ DOCKER_USER | default('dockeruser') }}"
check_mode: true
register: docker_user_info_final
tags: [info, instructions, always]

- name: Start Nginx container for testing
ansible.builtin.shell: |
export PATH=$HOME/bin:$HOME/.local/bin:$PATH
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
docker run -d --name test-nginx -p 8080:80 nginx:alpine
args:
executable: /bin/bash
become: true
become_user: "{{ DOCKER_USER | default('dockeruser') }}"
tags: [test, always]

- name: Verify Nginx service
ansible.builtin.shell: |
curl --retry 5 --retry-delay 2 -f http://localhost:8080
args:
executable: /bin/bash
become: true
become_user: "{{ DOCKER_USER | default('dockeruser') }}"
tags: [test, always]

- name: Cleanup test container
ansible.builtin.shell: |
export PATH=$HOME/bin:$HOME/.local/bin:$PATH
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
docker rm -f test-nginx
args:
executable: /bin/bash
become: true
become_user: "{{ DOCKER_USER | default('dockeruser') }}"
ignore_errors: true
tags: [test, always]


- name: Display Docker usage instructions
ansible.builtin.debug:
msg:
- "DOCKER USAGE INSTRUCTIONS:"
- "1. Switch to Docker user: sudo -u {{ DOCKER_USER | default('dockeruser') }} -i"
- "2. Start Docker daemon: systemctl --user start docker"
- "2. Check Docker daemon: systemctl --user status docker"
- "3. Test Docker: docker run hello-world"
- "4. For automatic startup: loginctl enable-linger {{ DOCKER_USER | default('dockeruser') }}"
- "5. Docker socket: /run/user/$(id -u)/docker.sock"
- "5. Docker socket: /run/user/{{ docker_user_info_final.uid }}/docker.sock"
- "6. Docker data: ~/.local/share/docker/"
- ""
- "SECURITY NOTES:"
- "- Docker daemon runs without root privileges"
- "- Containers cannot access host root filesystem"
- "- Limited to user's file permissions and capabilities"
- "- Network namespace isolation provides additional security"
- ""
- "TROUBLESHOOTING:"
- "If Docker fails to start, check:"
- "- AppArmor denials: sudo journalctl | grep -i apparmor | grep slirp4netns"
- "- Docker logs: journalctl --user -u docker -n 100"
- "- Docker rootless log: cat ~/docker-rootless.log"
- "- Test slirp4netns: /usr/bin/slirp4netns --help"
- "- To debug AppArmor, set DOCKER_APPARMOR_COMPLAIN_MODE=true and re-run"
tags: [info, instructions, always]
27 changes: 2 additions & 25 deletions setup-playbook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,8 @@
disable_root_account: "{{ DISABLE_ROOT_ACCOUNT | default(true) }}" # Disable root login
manage_users: "{{ MANAGE_USERS }}" # Enable user account management
manage_packages: true # Enable package management (removal/installation)

delete_users: ["games", "gnats", "irc", "list", "news", "sync", "uucp","ubuntu","debian"]

# === SYSTEM COMPONENT MANAGEMENT ===
# Each option controls whether the hardening role manages specific system components
manage_motdnews: "{{ MANAGE_MOTDNEWS | default(true) }}" # Message of the day
Expand Down Expand Up @@ -209,30 +210,6 @@
blocked_installed_packages: "{{ packages_blocklist }}"
tags: [hardening, security, cis-hardening]

# ===================================================================
# TEMPRORARY DEBUG PART
# Will be removed once https://github.com/konstruktoid/ansible-role-hardening/pull/979 is resolved and release is made
# ===================================================================

- name: Check if unattended-upgrades service exists and is enabled
ansible.builtin.systemd:
name: unattended-upgrades
register: unattended_upgrades_status
failed_when: false
become: true

- name: Restart unattended-upgrades to apply configuration changes
ansible.builtin.systemd_service:
name: unattended-upgrades
state: restarted
enabled: true
when:
- ansible_facts.virtualization_type not in ["container", "docker", "podman"]
- unattended_upgrades_status.status is defined
- unattended_upgrades_status.status.LoadState == "loaded"
- unattended_upgrades_status.status.UnitFileState in ["enabled", "enabled-runtime"]
become: true

# ===================================================================
# POST-CONFIGURATION SECTION
# Run any additional custom commands if specified
Expand Down
8 changes: 8 additions & 0 deletions templates/apparmor-local-unprivileged_userns.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Minimal Docker rootless compatibility
capability setuid,
capability setgid,

# Allow user namespace mapping files (owner-restricted)
owner @{PROC}/@{pid}/uid_map w,
owner @{PROC}/@{pid}/gid_map w,
owner @{PROC}/@{pid}/setgroups w,
Loading