New page documenting the majormail (2026-06-05) issue: /etc/localtime shipped labeled etc_t instead of locale_t on the Hetzner image, so SELinux denied systemd-timedated and timedatectl/community.general.timezone reported success while the symlink stayed at UTC. Fix: restorecon before setting TZ. Indexed in index.md (SELinux) + SUMMARY.md.
4.5 KiB
| title | domain | category | tags | status | created | updated | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| SELinux: Wrong /etc/localtime Label Silently Breaks Timezone Changes | troubleshooting | general |
|
published | 2026-06-05 | 2026-06-05 |
SELinux: Wrong /etc/localtime Label Silently Breaks Timezone Changes
timedatectl set-timezone (and Ansible's community.general.timezone) report success but the timezone never actually changes — date keeps showing the old zone. The cause is an SELinux mislabel on /etc/localtime: it must be locale_t, but freshly-provisioned images sometimes ship it as etc_t, which makes SELinux deny systemd-timedated from rewriting the symlink.
Hit on majormail (Fedora 44, SELinux Enforcing, Hetzner Cloud image), 2026-06-05. The box stayed on UTC for hours despite the timezone task "succeeding."
Symptoms
timedatectl set-timezone America/New_Yorkexits 0, butdatestill shows the old zone/offset.timedatectl show -p Timezone --valuereports the new zone whilereadlink /etc/localtimestill points at the old one — an inconsistent split state.- Ansible
community.general.timezonereportschanged=false("already set") because its idempotence check reads the stale in-memory value fromtimedatectl. journalctl -u systemd-timedatedshows:Failed to set time zone: Permission denied.- A direct
ln -sf … /etc/localtimeworks — but a brand-new symlink may get the wrong label again, sending you in circles.
Why It Happens
systemd-timedated changes the timezone by replacing the /etc/localtime symlink. Under SELinux Enforcing, that target must be labeled locale_t. If it is etc_t (or anything else), timedated is denied (Permission denied) and aborts — but timedatectl/the Ansible module surface this poorly, so the change looks like it took. The denial may be dontaudit-suppressed, so ausearch -m avc can come up empty, hiding the real cause.
Diagnosis
# The split state — these two should agree but won't:
readlink /etc/localtime # e.g. .../Etc/UTC (the truth)
timedatectl show -p Timezone --value # e.g. America/New_York (stale)
date '+%Z %z' # confirms actual zone via the symlink
# The label — this is the smoking gun:
ls -Z /etc/localtime # WRONG: ...:etc_t:s0
matchpathcon /etc/localtime # EXPECTED: ...:locale_t:s0
# The denial (only if dontaudit is disabled):
journalctl -u systemd-timedated | grep -i 'permission denied'
Fix
Relabel first, then set the timezone the normal way:
restorecon -v /etc/localtime # etc_t -> locale_t
timedatectl set-timezone America/New_York
# verify all three agree now:
date '+%F %T %Z (%z)'; readlink /etc/localtime; ls -Z /etc/localtime
If you set the symlink by hand (ln -sf) as a stopgap, run restorecon /etc/localtime afterward — a manually created symlink can inherit etc_t and re-break the next timedatectl call.
Then restart anything that caches the zone at startup so logs/schedules switch over:
systemctl restart rsyslog crond
(journalctl renders in local time automatically; rsyslog-written logs like /var/log/maillog keep the old zone until rsyslog restarts.)
Codify (Ansible)
Run restorecon on /etc/localtime before the timezone task, so a mislabeled symlink can't silently defeat it:
- name: Ensure correct SELinux label on /etc/localtime
ansible.builtin.command: restorecon -v /etc/localtime
register: localtime_relabel
changed_when: "'Relabeled' in localtime_relabel.stdout"
when: ansible_selinux.status | default('disabled') == 'enabled'
- name: Set timezone
community.general.timezone:
name: America/New_York
On majormail this is in roles/majormail/tasks/main.yml (MajorAnsible commit 2ff566d).
Key Notes
timedatectl/the Ansible module lie here. Always confirm withreadlink /etc/localtime+date, not justtimedatectl show.- The denial can be invisible. dontaudit rules may hide the AVC; trust the label mismatch (
ls -Zvsmatchpathcon) over an emptyausearch. - Fresh cloud images are the usual offender — a clean rebuild/provision is where the wrong label sneaks in.