Skip to content

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_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.