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.
92 lines
4.5 KiB
Markdown
92 lines
4.5 KiB
Markdown
---
|
|
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)
|