ansible/ansible · error · UnarchiveError
Cannot change group ownership of
Error message
Cannot change group ownership of %s to %s, as user %s
What it means
Raised by the unarchive module when the archive contains files whose group ownership would need to change to a group that the executing (non-root) user does not belong to. The code first checks run_uid != 0, then verifies whether the future group/gid differs from the run group and is absent from the user's supplementary groups. It is an UnarchiveError that aborts the task before any extraction drift occurs.
Solutions
- Run the task with become: true (root can chgrp to any group), or delegate to a host where the user has the needed rights
- Add the executing user to the target group (usermod -aG <group> <user>) and re-login so the supplementary group list includes fut_gid
- Rebuild/normalize the archive so its group ownership matches the extracting user's group (tar --group= on creation, or chgrp -R the tree before archiving)
- Set group ownership/sticky behaviors via the module's owner/group arguments consistent with the run user's memberships
Example fix
# before
- name: extract app tarball
ansible.builtin.unarchive:
src: app.tar.gz
dest: /opt/app
# runs as non-root 'deploy', archive gid is 'apache'
# after
- name: extract app tarball
ansible.builtin.unarchive:
src: app.tar.gz
dest: /opt/app
become: true
# or: add deploy to apache group and re-run Defensive patterns
Strategy: validation
Validate before calling
# Before unarchive as non-root, confirm membership in the archive's group
import grp, os
def can_chgrp(path_gid, run_gid=None):
run_gid = run_gid if run_gid is not None else os.getgid()
groups = {g.gr_gid for g in grp.getgrall() if os.getlogin() in g.gr_mem}
groups.add(run_gid)
return path_gid in groups or os.getuid() == 0
# in the task: fail fast with a clear message
- ansible.builtin.stat: {path: /tmp/app.tar.gz}
register: st Prevention
- Run unarchive with become: true when archives carry foreign group ownership
- Standardize archive creation with tar --group=<run_group> so ownership matches the extracting user
- Document required group memberships in role meta and assert them with a pre-task checking `id -nG`
- Avoid check_mode surprises: this ownership guard is evaluated even when nothing is extracted
When it happens
Trigger: Running unarchive as a non-root user (no become) where the archive records a group different from the run group and the fut_gid is not in the groups obtained for the user; e.g. archive built with group 'apache' extracted by user 'deploy' that is only in 'deploy'.
Common situations: CI pipelines extracting tarballs as an unprivileged service user; archives created on a different host with different group names/gids; hardening setups that forbid become; check_mode runs still evaluate this ownership guard.
Related errors
- Cannot change ownership of
- Could not make backup of
- Could not update files in
- Could not write data to file
- Failed to read .
AI-assisted analysis of ansible/ansible@9cf16a4aca (2026-08-15).
Data as JSON: /api/errors/1292a460df075508.
Report an issue: GitHub.
Appendix: source
Thrown at lib/ansible/modules/unarchive.py:740
if owner and owner != fut_owner:
change = True
err += 'Path %s is owned by user %s, not by user %s as expected\n' % (path, owner, fut_owner)
itemized[6] = 'o'
elif uid and uid != fut_uid:
change = True
err += 'Path %s is owned by uid %s, not by uid %s as expected\n' % (path, uid, fut_uid)
itemized[6] = 'o'
# Compare file group ownership
group = gid = None
try:
group = grp.getgrgid(st.st_gid).gr_name
except (KeyError, ValueError, OverflowError):
gid = st.st_gid
if run_uid != 0 and (fut_group != run_group or fut_gid != run_gid) and fut_gid not in groups:
raise UnarchiveError('Cannot change group ownership of %s to %s, as user %s' % (path, fut_group, run_owner))
if group and group != fut_group:
change = True
err += 'Path %s is owned by group %s, not by group %s as expected\n' % (path, group, fut_group)
itemized[6] = 'g'
elif gid and gid != fut_gid:
change = True
err += 'Path %s is owned by gid %s, not by gid %s as expected\n' % (path, gid, fut_gid)
itemized[6] = 'g'
# Register changed files and finalize diff output
if change:
if path not in self.includes:
self.includes.append(path)
diff += '%s %s\n' % (''.join(itemized), path)
if self.includes:
unarchived = FalseView on GitHub (pinned to 9cf16a4aca)