Skip to content

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 .Blocked real-time Bayes learn-on-move scripts trained fine for a day, then an unrelated restorecon (from a fail2ban SELinux fix) reverted their labels and killed training. Surfaced only because the per-minute selinux-avc-notify timer emailed the denials.

Symptoms

  • The sieve pipe action silently no-ops. Whatever the script does (here: sa-learn Bayes training) just stops happening — no bounce, no error to the mail client.
  • selinux-avc-notify / ausearch shows denials like:
    avc: denied { execute } for comm=imap name=sa-learn-unblocked-ham.sh
      scontext=system_u:system_r:dovecot_t:s0
      tcontext=system_u:object_r:dovecot_etc_t:s0 tclass=file permissive=0
    
  • The script's own log shows it worked for a while, then stops abruptly at the time of a relabel.
  • ls -Z on the script shows dovecot_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 fcontext rule is temporary. setype: on copy, chcon, or an inherited label all get reverted by the next restorecon. Always pin it with semanage 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 restorecon that reverts it. Don't assume the last person to touch the mail config caused it.
  • bin_t needs no custom policy for Dovecot pipe scripts — dovecot_t already has execute_no_trans on bin_t. Reach for a .te module 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.