Mail Submission Ports That Refuse Plaintext
An EL host opens no submission service, so every client sends on port 25 where plaintext is accepted. This role adds 587, which refuses mail until the session is encrypted, and 465, which is TLS from the first byte. Port 25 keeps taking plaintext on purpose and the live test asserts it, because a mail exchanger that demands STARTTLS loses mail. Original role, live-tested on Rocky Linux 10.
Verification
Live-testedReally deployed to a container sandbox, proven idempotent (a second run changes nothing), verified against the role’s assertions, then torn down.
Conformance
- Static validation (yamllint · ansible-lint)
Provenance
- SHA-256 checksum
- Signature (pending)
Functional
- Live-tested - applied, verified, destroyed
Last verified 2026-09-27 · podman 4.9.3 · ansible 2.21.4 · how we verify
Cite it in your README
badge · attributionPaste this beside the module in the repository that uses it. The badge is rendered from this artifact's verification record, so it reads live-tested because the record says so, and the link lands on this page.
[](https://www.iac-bazaar.com/catalog/ansible-postfix-tls?utm_source=syndication&utm_medium=readme&utm_campaign=artifact)
Ansible role 1.0.0, live-tested on IaC Bazaar: [Mail Submission Ports That Refuse Plaintext](https://www.iac-bazaar.com/catalog/ansible-postfix-tls?utm_source=syndication&utm_medium=readme&utm_campaign=artifact)
```yaml
# Mail Submission Ports That Refuse Plaintext: https://www.iac-bazaar.com/catalog/ansible-postfix-tls (download from your IaC Bazaar account)
```Preview:
Documentation
postfix-tls
An EL host has no mail submission service, so every client sends on port 25 - where plaintext is accepted by design. The package's own configuration does enable opportunistic TLS there, with a certificate it creates at install time, so a client that asks for STARTTLS gets TLS 1.3 and one that does not asks for nothing and is served anyway. This role adds the two ports where encryption is not optional: 587, which refuses mail until the session is encrypted, and 465, which is TLS from the first byte.
No download, and no version to pin. postfix is an EL 10 package. What the
role owns is a handful of main.cf parameters - written with postconf, Postfix's
own editor, so the file keeps its documentation - two master.cf services, and the
proof that each port accepts and refuses what it should.
Port 25 is left accepting plaintext on purpose, and the live test asserts it. A mail exchanger that demanded STARTTLS would refuse mail from every sender that does not offer it, silently, and that is a worse outcome than an unencrypted hop. Mandatory encryption belongs on the submission ports, where the clients are yours.
This role does not enable TLS, because the package already did. Rocky's
main.cf ships smtpd_tls_security_level = may and names
/etc/pki/tls/certs/postfix.pem, and that certificate is really there after
installation - measured on a fresh host, where STARTTLS on port 25 negotiated TLS
1.3 before this role ran at all. What is missing on a stock host is a port where
TLS is required, and that is what this adds.
The TLS 1.0 refusal on EL 10 is the crypto policy's work, not Postfix's.
smtpd_tls_mandatory_protocols is >=TLSv1 out of the box, while the DEFAULT
system crypto policy sets TLS.MinProtocol = TLSv1.2 - so an openssl s_client -tls1 fails before it reaches Postfix, with "no protocols available" from the
CLIENT. The role sets >=TLSv1.2 in Postfix as well, so the floor is stated
where a reader looks for it, but the README says plainly which component is doing
the refusing. A catalogue entry claiming otherwise would be taking credit for the
platform, which is a mistake this catalogue has made once already and written
down.
postfix check catches syntax, not spelling. A malformed line gives rc 1,
names the line number - "missing '=' after attribute name" - and postfix then
fails to start. A well-formed parameter it has never heard of gets a warning and
rc 0. So the role runs the check every time AND the live test holds real SMTP
conversations afterwards, because a configuration can pass the check and still
serve the wrong thing.
postconf -e says nothing about whether it changed anything. Every parameter
is read with postconf -h first and written only when it differs, which is what
keeps the role idempotent; the same applies to master.cf, where postconf -M
prints nothing at all for a service that is not defined.
inet_interfaces is not touched. The package ships localhost, so the two
new ports listen on loopback until somebody widens it deliberately. A role that
opened a mail port to the network as a side effect of hardening TLS would be
doing something nobody asked for.
License
Commercial - IaC Bazaar EULA. (c) IaC Bazaar.
Usage code & full reference need an account
The complete copy-paste usage, the full input/output reference, and operational notes are free with an account - shown here and bundled in the download. Sign in and this section fills in.
- Variables
- Test
Related modules
ansible-kafka
Apache Kafka 4 (SHA-512 pinned) on Java 21, one KRaft node on EL 10 from the upstream release, as a hardened systemd service on loopback; the live test creates a topic, produces one message, consumes it back and reads the metadata quorum; the metadata log is formatted once as the service user and the root-owned release tree is run in place. Original role, live-tested on Rocky Linux 10.
ansible-gotify
Gotify on EL 10 from the vendor's release, pinned by a SHA-256 the role carries (the vendor publishes no checksum file), as a hardened systemd service on loopback with its settings in an EnvironmentFile; the live test creates an application, pushes a message with its token, reads it back as the admin, and sees a wrong password and a bad token refused. Original role, live-tested on Rocky Linux 10.
ansible-mosquitto-broker
Mosquitto (MQTT) with accounts: the package starts in local-only mode with anonymous clients allowed, no persistence directory and no packet-size limit. This role writes a listener that refuses anonymous and wrong-password clients (proved live), keeps retained messages across restarts, caps packets at 1 MiB, and renders topic ACLs from a list. Original role, live-tested on Rocky Linux 10.
ansible-nats
nats on EL 10 from the GitHub release; get_url refuses it unless its SHA-256 is in the vendor's checksums file, and the live test re-checks it, then saves a context under a throwaway config home, reads it back, and runs server check connection against a dead port expecting exit 2 and CRITICAL. Pinned. Original role, live-tested on Rocky Linux 10.
ansible-nats-server
NATS server from the upstream release (sha256-verified) as a hardened system service on loopback with JetStream under its own data directory and the configuration checked by nats-server -t before it lands. No client is installed, so the live test speaks the protocol itself: one session subscribes, publishes and reads its own message back. Original role, live-tested on Rocky Linux 10.
ansible-nsq
NSQ (SHA-256 pinned): nsqd and nsqlookupd as two hardened services from one release on EL 10 from the upstream release, as a hardened systemd service on loopback; the live test publishes four messages, sees the topic count them with the channel holding the last, and asks the directory which broker holds the topic. Original role, live-tested on Rocky Linux 10.