From d10db317c158f9892b587ad618fd9a5e19aa3fb0 Mon Sep 17 00:00:00 2001 From: jdaln <150942337+jdaln@users.noreply.github.com> Date: Wed, 29 Oct 2025 14:02:17 +0100 Subject: [PATCH 1/4] update version for docker run and adapt playbook --- ansible.cfg | 6 +++++- install-docker-rootless.yml | 43 ++++++++++--------------------------- setup-playbook.yml | 3 ++- 3 files changed, 18 insertions(+), 34 deletions(-) diff --git a/ansible.cfg b/ansible.cfg index fa51fc2..e87bb03 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -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/ diff --git a/install-docker-rootless.yml b/install-docker-rootless.yml index 2c6ae07..166fe1b 100644 --- a/install-docker-rootless.yml +++ b/install-docker-rootless.yml @@ -37,9 +37,10 @@ - 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 @@ -58,15 +59,6 @@ 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] tasks: # =================================================================== @@ -95,7 +87,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.2.1' # Pinned version for reproducibility delegate_to: localhost run_once: true tags: [docker-setup, role-download] @@ -116,8 +108,7 @@ 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) @@ -125,32 +116,20 @@ docker_rootful: "{{ DOCKER_ROOTFUL | default(false) }}" # Install rootful Docker alongside rootless docker_rootful_opts: "{{ DOCKER_ROOTFUL_OPTS | default(false) }}" # Additional rootful Docker options + # docker_driver_network: "{{ DOCKER_DRIVER_NETWORK | default('vpnkit') }}" + # === SERVICE MANAGEMENT === - docker_service_restart: "{{ DOCKER_SERVICE_RESTART | default(true) }}" # Restart Docker service after installation + docker_service_restart: false # avoid systemd --user inside role; we handle start in post-tasks 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 - register: docker_version_check - changed_when: false - failed_when: false - tags: [validation, post-check] - - - name: Check Docker daemon status for user - ansible.builtin.command: | - sudo -u {{ DOCKER_USER | default('dockeruser') }} systemctl --user is-active docker - register: docker_service_check - changed_when: false - failed_when: false - tags: [validation, post-check] - name: Display Docker installation summary ansible.builtin.debug: @@ -158,7 +137,7 @@ - "===== 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) }}" @@ -173,7 +152,7 @@ - "2. Start Docker daemon: systemctl --user start 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_uid }}/docker.sock" - "6. Docker data: ~/.local/share/docker/" - "" - "SECURITY NOTES:" diff --git a/setup-playbook.yml b/setup-playbook.yml index a961f16..7bc2c16 100644 --- a/setup-playbook.yml +++ b/setup-playbook.yml @@ -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 From 5f19cbe217eea7ddd53548d9daed5fab53899cd4 Mon Sep 17 00:00:00 2001 From: jdaln <150942337+jdaln@users.noreply.github.com> Date: Thu, 30 Oct 2025 09:33:02 +0100 Subject: [PATCH 2/4] removed old stuff --- setup-playbook.yml | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/setup-playbook.yml b/setup-playbook.yml index 7bc2c16..798b5dc 100644 --- a/setup-playbook.yml +++ b/setup-playbook.yml @@ -210,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 From 686dc8631294f2cc9fa68ff167b1243ccc1b67fc Mon Sep 17 00:00:00 2001 From: jdaln <150942337+jdaln@users.noreply.github.com> Date: Thu, 20 Nov 2025 15:31:29 +0100 Subject: [PATCH 3/4] docker rootless functional on Ubuntu (prob Debian too) --- install-docker-rootless.yml | 170 ++++++++++++++++++++++++++++++++++-- 1 file changed, 165 insertions(+), 5 deletions(-) diff --git a/install-docker-rootless.yml b/install-docker-rootless.yml index 166fe1b..817442c 100644 --- a/install-docker-rootless.yml +++ b/install-docker-rootless.yml @@ -59,6 +59,14 @@ failed_when: namespace_check.rc != 0 tags: [validation, always] + - 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: # =================================================================== @@ -87,7 +95,7 @@ ansible.builtin.git: repo: https://github.com/konstruktoid/ansible-role-docker-rootless.git dest: "{{ lookup('env', 'HOME') }}/.ansible/roles/konstruktoid.docker_rootless" - version: 'v1.2.1' # Pinned version for reproducibility + version: 'v1.6.0' # Pinned version for reproducibility delegate_to: localhost run_once: true tags: [docker-setup, role-download] @@ -116,10 +124,14 @@ docker_rootful: "{{ DOCKER_ROOTFUL | default(false) }}" # Install rootful Docker alongside rootless docker_rootful_opts: "{{ DOCKER_ROOTFUL_OPTS | default(false) }}" # Additional rootful Docker options - # docker_driver_network: "{{ DOCKER_DRIVER_NETWORK | default('vpnkit') }}" + # === 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: false # avoid systemd --user inside role; we handle start in post-tasks + 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 === @@ -131,6 +143,138 @@ # Post-installation validation and information post_tasks: + # NOTE: Network driver configuration is now handled by the role using: + # docker_driver_network: pasta + # docker_driver_port: implicit + # No wrapper script needed! + + + # AppArmor configuration for unprivileged_userns (REQUIRED for rootless Docker) + # We keep the profile in ENFORCE mode but add minimal required capabilities + + - 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: Backup original unprivileged_userns profile + ansible.builtin.copy: + src: /etc/apparmor.d/unprivileged_userns + dest: /etc/apparmor.d/unprivileged_userns.backup + remote_src: true + force: false # Don't overwrite existing backup + become: true + when: unprivileged_userns_apparmor_profile.stat.exists + tags: [docker-config, apparmor] + + - name: Comment out capability deny in unprivileged_userns profile + ansible.builtin.lineinfile: + path: /etc/apparmor.d/unprivileged_userns + regexp: '^(\s+)audit deny capability,$' + line: '\1# audit deny capability, # Commented for Docker rootless - capabilities controlled via local overrides' + backrefs: true + become: true + when: unprivileged_userns_apparmor_profile.stat.exists + register: unprivileged_userns_profile_patched + tags: [docker-config, apparmor] + + - 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.copy: + dest: /etc/apparmor.d/local/unprivileged_userns + mode: '0644' + content: | + # 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, + 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_profile_patched.changed or 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_profile_patched is defined and unprivileged_userns_profile_patched.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: [info, summary, always] + + - 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: [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: @@ -144,15 +288,23 @@ - "==================================================" 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: 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/{{ docker_user_uid }}/docker.sock" + - "5. Docker socket: /run/user/{{ docker_user_info_final.uid }}/docker.sock" - "6. Docker data: ~/.local/share/docker/" - "" - "SECURITY NOTES:" @@ -160,4 +312,12 @@ - "- 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] From 5cd180f5127209cb758e7cd9e6e5a215a0fbb831 Mon Sep 17 00:00:00 2001 From: jdaln <150942337+jdaln@users.noreply.github.com> Date: Fri, 21 Nov 2025 18:11:47 +0100 Subject: [PATCH 4/4] fixes --- install-docker-rootless.yml | 108 +++++++----------- .../apparmor-local-unprivileged_userns.j2 | 8 ++ 2 files changed, 50 insertions(+), 66 deletions(-) create mode 100644 templates/apparmor-local-unprivileged_userns.j2 diff --git a/install-docker-rootless.yml b/install-docker-rootless.yml index 817442c..a53526f 100644 --- a/install-docker-rootless.yml +++ b/install-docker-rootless.yml @@ -41,23 +41,12 @@ # 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: 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: @@ -143,43 +132,6 @@ # Post-installation validation and information post_tasks: - # NOTE: Network driver configuration is now handled by the role using: - # docker_driver_network: pasta - # docker_driver_port: implicit - # No wrapper script needed! - - - # AppArmor configuration for unprivileged_userns (REQUIRED for rootless Docker) - # We keep the profile in ENFORCE mode but add minimal required capabilities - - - 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: Backup original unprivileged_userns profile - ansible.builtin.copy: - src: /etc/apparmor.d/unprivileged_userns - dest: /etc/apparmor.d/unprivileged_userns.backup - remote_src: true - force: false # Don't overwrite existing backup - become: true - when: unprivileged_userns_apparmor_profile.stat.exists - tags: [docker-config, apparmor] - - - name: Comment out capability deny in unprivileged_userns profile - ansible.builtin.lineinfile: - path: /etc/apparmor.d/unprivileged_userns - regexp: '^(\s+)audit deny capability,$' - line: '\1# audit deny capability, # Commented for Docker rootless - capabilities controlled via local overrides' - backrefs: true - become: true - when: unprivileged_userns_apparmor_profile.stat.exists - register: unprivileged_userns_profile_patched - tags: [docker-config, apparmor] - - name: Ensure AppArmor local directory exists ansible.builtin.file: path: /etc/apparmor.d/local @@ -190,18 +142,10 @@ tags: [docker-config, apparmor] - name: Create minimal AppArmor local overrides for unprivileged_userns - ansible.builtin.copy: + ansible.builtin.template: + src: templates/apparmor-local-unprivileged_userns.j2 dest: /etc/apparmor.d/local/unprivileged_userns mode: '0644' - content: | - # 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, become: true when: unprivileged_userns_apparmor_profile.stat.exists register: unprivileged_userns_local_overrides @@ -213,7 +157,7 @@ become: true when: - unprivileged_userns_apparmor_profile.stat.exists - - unprivileged_userns_profile_patched.changed or unprivileged_userns_local_overrides.changed + - unprivileged_userns_local_overrides.changed tags: [docker-config, apparmor] - name: Configure Docker to use cgroupfs driver (bypasses systemd/polkit issues) @@ -240,7 +184,6 @@ become_user: "{{ DOCKER_USER | default('dockeruser') }}" when: - docker_daemon_config.changed - or (unprivileged_userns_profile_patched is defined and unprivileged_userns_profile_patched.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 @@ -296,6 +239,39 @@ 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: diff --git a/templates/apparmor-local-unprivileged_userns.j2 b/templates/apparmor-local-unprivileged_userns.j2 new file mode 100644 index 0000000..1fb9724 --- /dev/null +++ b/templates/apparmor-local-unprivileged_userns.j2 @@ -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,