--- title: "SELinux: Wrong /etc/localtime Label Silently Breaks Timezone Changes" domain: troubleshooting category: general tags: [selinux, timezone, timedatectl, localtime, fedora, ansible, hetzner] status: published created: 2026-06-05 updated: 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_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 ```bash # 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: ```bash 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: ```bash 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: ```yaml - 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. ## Related - [SELinux: Fixing Dovecot Mail Spool Context (/var/vmail)](selinux-dovecot-vmail-context.md) - [Dovecot IMAP Clients Fail to Sync: vsz_limit OOM from a Bloated Index Log](networking/dovecot-imap-oom-vsz-limit-bloated-index.md)