SELinux: Dovecot Sieve-Pipe Scripts Break After restorecon (Missing bin_t fcontext Rule)¶
A Dovecot IMAPSieve (or LDA/LMTP sieve) extprograms pipe script works right after you deploy it, then silently stops firing days later — with SELinux AVC denials showing dovecot_t denied execute on the script. The script's label reverted to the directory default (dovecot_etc_t), which dovecot_t may not execute. The real bug is that the working label was never backed by a persistent semanage fcontext rule, so the next restorecon anywhere on the tree reset it.
Hit on majormail (Fedora 44, SELinux Enforcing), 2026-07-01. The
.Blockedreal-time Bayes learn-on-move scripts trained fine for a day, then an unrelatedrestorecon(from a fail2ban SELinux fix) reverted their labels and killed training. Surfaced only because the per-minuteselinux-avc-notifytimer emailed the denials.
Symptoms¶
- The sieve pipe action silently no-ops. Whatever the script does (here:
sa-learnBayes training) just stops happening — no bounce, no error to the mail client. selinux-avc-notify/ausearchshows denials like:- The script's own log shows it worked for a while, then stops abruptly at the time of a relabel.
ls -Zon the script showsdovecot_etc_t— not an executable type.- It "worked before" — because a fresh
copy/create happened to land the right label, but nothing made it stick.
Why It Happens¶
Dovecot's sieve extprograms plugin runs pipe scripts (sieve_pipe_bin_dir, conventionally /usr/lib/dovecot/sieve-pipe/) in the calling Dovecot domain — for IMAPSieve that's dovecot_t (the imap process). For dovecot_t to run the script, the file needs a type dovecot_t is allowed to execute (e.g. bin_t).
But /usr/lib/dovecot/sieve-pipe/ has no shipped fcontext rule, so its default type is dovecot_etc_t (a config type, not executable). If you place a script there and it happens to carry bin_t at deploy time (e.g. inherited, or set once by hand), it runs — until the next restorecon -R on /usr/lib or /etc (a reboot autorelabel, a policy update, or any unrelated SELinux fix elsewhere) resets it to the default dovecot_etc_t. Then dovecot_t is denied execute and the pipe silently fails.
The general lesson: an SELinux label that isn't backed by a persistent semanage fcontext rule is temporary. restorecon will eventually revert it. (Same failure class as a config file mv'd from /tmp keeping tmp_t, and localtime shipping as etc_t.)
Diagnosis¶
# The smoking gun — label is a config type, not executable:
ls -Z /usr/lib/dovecot/sieve-pipe/*.sh
# ...:dovecot_etc_t:s0 <- WRONG (dovecot_t can't execute this)
# Is there a persistent rule? (empty output = nothing pins it = it will revert)
semanage fcontext -l | grep sieve-pipe
# The denials:
ausearch -m AVC -ts recent | grep -E 'sieve|sa-learn'
# Confirm dovecot_t is actually allowed to execute bin_t (it is, in-domain):
sesearch --allow -s dovecot_t -t bin_t -c file
# allow dovecot_domain base_ro_file_type:file { execute execute_no_trans map };
# ^ execute_no_trans = runs in dovecot_t, no domain transition needed. Exactly what a pipe script wants.
bin_t is the correct target: dovecot_t gets execute + execute_no_trans on it via corecmd_exec_bin (the base_ro_file_type attribute), so the script runs in-domain with no extra policy module needed.
Fix¶
Add a persistent fcontext rule, then relabel:
semanage fcontext -a -t bin_t '/usr/lib/dovecot/sieve-pipe(/.*)?\.sh'
restorecon -Rv /usr/lib/dovecot/sieve-pipe
# Relabeled .../sa-learn-unblocked-ham.sh from ...dovecot_etc_t:s0 to ...bin_t:s0
# verify it's pinned:
ls -Z /usr/lib/dovecot/sieve-pipe/*.sh # now bin_t
semanage fcontext -l | grep sieve-pipe # rule present -> survives future relabels
No custom .te policy module is required — bin_t + the stock dovecot_t allow rules are sufficient. Do not "fix" this by setting the label with chcon or by relying on copy's setype alone; both are non-persistent and will revert on the next relabel. The semanage fcontext rule is what makes it durable.
Codify (Ansible)¶
Two parts — set the label at deploy time and persist the rule so a relabel can't undo it:
# tasks: deploy the pipe scripts with the right type immediately
- name: Deploy IMAPSieve pipe wrapper scripts
ansible.builtin.copy:
src: "sieve-pipe/{{ item }}"
dest: "/usr/lib/dovecot/sieve-pipe/{{ item }}"
owner: root
group: root
mode: '0755'
setype: bin_t # correct label at deploy time
loop:
- sa-learn-blocked-spam.sh
- sa-learn-unblocked-ham.sh
# selinux tasks: the DURABLE piece — persistent rule + relabel handler
- name: Label IMAPSieve pipe scripts as bin_t (dovecot_t must execute them)
community.general.sefcontext:
target: '/usr/lib/dovecot/sieve-pipe(/.*)?\.sh'
setype: bin_t
state: present
notify: restorecon sieve-pipe
# handlers
- name: restorecon sieve-pipe
ansible.builtin.command: restorecon -Rv /usr/lib/dovecot/sieve-pipe
listen: restorecon sieve-pipe
Flush the relabel handler before re-asserting SELINUX=enforcing in the same run. On majormail this lives in roles/majormail/tasks/{dovecot,selinux}.yml + handlers (MajorAnsible commit 79c22dd).
Key Notes¶
- A label without a
semanage fcontextrule is temporary.setype:oncopy,chcon, or an inherited label all get reverted by the nextrestorecon. Always pin it withsemanage fcontext -a. - The break is time-delayed and looks unrelated. It works at deploy, then a different SELinux fix (or a reboot autorelabel) elsewhere triggers the
restoreconthat reverts it. Don't assume the last person to touch the mail config caused it. bin_tneeds no custom policy for Dovecot pipe scripts —dovecot_talready hasexecute_no_transonbin_t. Reach for a.temodule only if the script must run in its own domain.- You'll only notice if you monitor AVCs. A silent pipe failure has no user-facing symptom; a per-minute AVC-notify timer (or netdata AVC chart) is what turns "spam training quietly died" into an actionable alert.