majorwiki/05-troubleshooting/selinux-localtime-label-breaks-timezone.md
MajorLinux d755b77126 troubleshooting: SELinux /etc/localtime mislabel silently breaks timezone
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.
2026-06-05 14:22:00 -04:00

4.5 KiB

title domain category tags status created updated
SELinux: Wrong /etc/localtime Label Silently Breaks Timezone Changes troubleshooting general
selinux
timezone
timedatectl
localtime
fedora
ansible
hetzner
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 changesdate 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_York exits 0, but date still shows the old zone/offset.
  • timedatectl show -p Timezone --value reports the new zone while readlink /etc/localtime still points at the old one — an inconsistent split state.
  • Ansible community.general.timezone reports changed=false ("already set") because its idempotence check reads the stale in-memory value from timedatectl.
  • journalctl -u systemd-timedated shows: Failed to set time zone: Permission denied.
  • A direct ln -sf … /etc/localtime works — 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 with readlink /etc/localtime + date, not just timedatectl show.
  • The denial can be invisible. dontaudit rules may hide the AVC; trust the label mismatch (ls -Z vs matchpathcon) over an empty ausearch.
  • Fresh cloud images are the usual offender — a clean rebuild/provision is where the wrong label sneaks in.