diff --git a/README.md b/README.md
index 99036f0b7db851d716248d3042979e500f982993..04ee72b45110c17ca595613e1a88d1792e2ffde0 100644
--- a/README.md
+++ b/README.md
@@ -42,6 +42,7 @@ order:
 - `ansible-playbook 02_install_nvidia_driver_*.yml`
 - `ansible-playbook 03_install_mellanox_ofed.yml`
 - `ansible-playbook 04_install_slurm.yml`
+- `ansible-playbook xx_merge_users.yml`
 
 ### Playbook overview
 
@@ -63,5 +64,10 @@ order:
 - **03_install_mellanox_ofed.yml**\
   Installs Mellanox OFED drivers.
 
-- **04\_ install_slurm.yml**\
+- **04_install_slurm.yml**\
   Installs and configures SLURM for use with DAS-6.
+
+- **xx_merge_users.yml**\
+  Merges the system users from the local machine with the users from DAS-6. This
+  playbook has number `xx` because it should preferably be ran periodically,
+  e.g. using cron.
diff --git a/templates/merge_users.sh b/templates/merge_users.sh
new file mode 100644
index 0000000000000000000000000000000000000000..848165590dc850c4498bc41157f7615e5181e1df
--- /dev/null
+++ b/templates/merge_users.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+set -e
+
+SOURCE_DIR=$1
+DEST_DIR=$2
+
+for file in passwd group shadow; do
+    SOURCE_FILE="${SOURCE_DIR}/${file}_src"
+    DEST_FILE="${DEST_DIR}/${file}"
+    MERGED_FILE="${SOURCE_DIR}/merged_${file}"
+
+    # Ensure files exist
+    if [[ ! -f "$SOURCE_FILE" || ! -f "$DEST_FILE" ]]; then
+        echo "Error: One of the source or destination files is missing!"
+        exit 1
+    fi
+
+    # Extract users
+    awk -F: '$3 < 1000' "$DEST_FILE" > "$MERGED_FILE"  # System users from target
+    awk -F: '$3 >= 1000' "$SOURCE_FILE" >> "$MERGED_FILE"  # Normal users from source
+
+    # Ensure uniqueness and sort
+    sort -t: -k1,1 -u -o "$MERGED_FILE" "$MERGED_FILE"
+done
+
diff --git a/xx_merge_users.yml b/xx_merge_users.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ec2bd2e03fd88bb0928888423d7d11f1eaf0a360
--- /dev/null
+++ b/xx_merge_users.yml
@@ -0,0 +1,54 @@
+- name: Merge passwd, group, and shadow files from fs5 to target system
+  hosts: localhost
+  become: true
+  vars:
+    root_host: fs5
+    files:
+      - {name: "passwd", mode: "0644"}
+      - {name: "group", mode: "0644"}
+      - {name: "shadow", mode: "0600"}
+    local_temp_dir: /tmp/user_merge
+    remote_etc_dir: /etc
+  tasks:
+    - name: Ensure local temporary directory exists
+      file:
+        path: "{{ local_temp_dir }}"
+        state: directory
+        mode: '0755'
+    - name: Fetch user-related files from fs5
+      fetch:
+        src: "{{ remote_etc_dir }}/{{ item.name }}"
+        dest: "{{ local_temp_dir }}/{{ item.name }}_fs5"
+        flat: true
+      loop: "{{ files }}"
+      delegate_to: "{{ root_host }}"
+      vars:
+        ansible_ssh_common_args: "-o StrictHostKeyChecking=no"
+    - name: Read current target system files
+      slurp:
+        src: "{{ remote_etc_dir }}/{{ item.name }}"
+      register: target_files
+      loop: "{{ files }}"
+    - name: Generate user merge script
+      template:
+        src: merge_users.sh
+        dest: "{{ local_temp_dir }}/merge_users.sh"
+        mode: '0755'
+    - name: Execute merge script
+      command: "{{ local_temp_dir }}/merge_users.sh {{ local_temp_dir }} {{ remote_etc_dir }}"
+      register: merge_output
+      changed_when: merge_output.rc == 0
+    - name: Replace files if changed
+      copy:
+        src: "{{ local_temp_dir }}/merged_{{ item.name }}"
+        dest: "{{ remote_etc_dir }}/{{ item.name }}"
+        owner: root
+        group: root
+        mode: "{{ item.mode }}"
+        backup: true
+      when: target_files.results | selectattr('item.name', 'equalto', item.name) | map(attribute='content') | map('b64decode') | first != lookup('file', '{{ local_temp_dir }}/merged_{{ item.name }}')
+      loop: "{{ files }}"
+    - name: Clean up temporary files
+      file:
+        path: "{{ local_temp_dir }}"
+        state: absent