Mail Server Setup - Technical Documentation
Table of Contents
- Overview
- System Configuration
- Package Installation
- Postfix Mail Transfer Agent
- SSL/TLS Configuration
- Anti-Spam Measures
- SASL Authentication
- Submission Port (587)
- Dovecot IMAP/POP3 Server
- OpenDKIM Email Signing
- Email Aliases
- User Creation
- Firewall Configuration
- Service Startup
- Helper Scripts
- DNS Configuration Timer
- SSL Certificate Acquisition
Overview
This script sets up a complete, production-ready mail server with the following components:
- Postfix: Mail Transfer Agent (MTA) for sending/receiving email
- Dovecot: IMAP/POP3 server for email retrieval
- OpenDKIM: Email authentication and signing
- Let's Encrypt: Free SSL/TLS certificates
- Anti-spam: RBL checks, rate limiting, and authentication requirements
- Security: Encrypted connections, authentication, firewall rules
The setup achieves a 9/10 mail-tester.com score and ensures high deliverability.
System Configuration
Package Management Configuration
sed -i "s/#\$nrconf{restart} = 'i';/\$nrconf{restart} = 'a';/" /etc/needrestart/needrestart.conf
Purpose: Configures automatic service restarts during package updates.
needrestartnormally prompts interactively when services need restarting- Setting to 'a' (automatic) allows unattended installation
- Critical for automated deployments via StackScript
echo 'Dpkg::Options {"--force-confold";}' > /etc/apt/apt.conf.d/99-force-confold
Purpose: Preserves existing configuration files during package upgrades.
- When packages are upgraded, dpkg may ask about config file changes
--force-confoldkeeps the currently installed version- Prevents installation from hanging on configuration prompts
System Updates
apt-get update -qq
apt-get upgrade -y -qq -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold"
Purpose: Updates package lists and upgrades all installed packages.
-qq: Quiet mode, minimal output-y: Automatic yes to prompts--force-confdef: Use default for new config files--force-confold: Keep old config files when unchanged- Ensures system has latest security patches
Hostname Configuration
hostnamectl set-hostname "$HOSTNAME"
Purpose: Sets the system hostname (e.g., mx.itisajoke.net).
- Used by Postfix to identify itself in SMTP communications
- Appears in email headers
- Must match your PTR (reverse DNS) record
grep -q "$HOSTNAME" /etc/hosts || echo "127.0.1.1 $HOSTNAME mail" >> /etc/hosts
Purpose: Adds hostname to /etc/hosts for local resolution.
127.0.1.1is the standard loopback for hostname- Ensures hostname resolves locally without DNS lookup
mailalias provides additional local namegrep -qchecks if entry exists to avoid duplicates
Package Installation
Pre-configuration
debconf-set-selections <<< "postfix postfix/mailname string $DOMAIN"
Purpose: Pre-configures Postfix's mail domain name.
- Prevents interactive prompts during installation
- Sets the domain appended to local user emails
- Example:
userbecomesuser@itisajoke.net
debconf-set-selections <<< "postfix postfix/main_mailer_type string 'Internet Site'"
Purpose: Configures Postfix for direct internet mail delivery.
- Internet Site: Sends and receives mail directly
- Alternative would be "Satellite" (relay through another server)
- Required for running an independent mail server
debconf-set-selections <<< "dovecot-core dovecot-core/create-ssl-cert boolean true"
debconf-set-selections <<< "dovecot-core dovecot-core/ssl-cert-name string $HOSTNAME"
Purpose: Pre-configures Dovecot's SSL certificate settings.
- Creates initial self-signed certificate during installation
- Sets certificate CN (Common Name) to hostname
- Later replaced with Let's Encrypt certificate
Package Installation
apt-get install -y -qq postfix dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd \
certbot mailutils dnsutils opendkim opendkim-tools
Packages installed:
- postfix: SMTP server for sending/receiving mail
- dovecot-core: Base IMAP/POP3 server
- dovecot-imapd: IMAP protocol support (port 143/993)
- dovecot-pop3d: POP3 protocol support (port 110/995)
- dovecot-lmtpd: Local Mail Transfer Protocol (Postfix → Dovecot delivery)
- certbot: Let's Encrypt SSL certificate manager
- mailutils: Command-line mail utilities (mail, mailx commands)
- dnsutils: DNS lookup tools (dig, nslookup)
- opendkim: DKIM signing daemon
- opendkim-tools: DKIM key generation and testing utilities
Stop Services
systemctl stop postfix dovecot opendkim 2>/dev/null || true
Purpose: Stops services before configuration.
- Prevents services from reading partially-written config files
2\>/dev/nullsuppresses errors if services aren't running|| trueprevents script failure if stop fails
Postfix Mail Transfer Agent
Basic Configuration
postconf -e "myhostname = $HOSTNAME"
Purpose: Sets the mail server's hostname.
- Used in SMTP HELO/EHLO commands
- Appears in
Received:headers - Must match PTR record for best deliverability
- Example:
mx.itisajoke.net
postconf -e "mydomain = $DOMAIN"
Purpose: Sets the base domain for the mail server.
- Used for email addresses on this server
- Example:
itisajoke.net - Local usernames become
user@itisajoke.net
postconf -e 'myorigin = $mydomain'
Purpose: Sets the domain that appears in outgoing mail FROM addresses.
- When local users send mail, this domain is appended
rootsends asroot@itisajoke.net(notroot@mx.itisajoke.net)- Uses the
$mydomainvariable defined above
postconf -e 'inet_interfaces = all'
Purpose: Listens on all network interfaces.
- all: Accepts connections from any interface
- localhost: Would only accept local connections
- Required for receiving mail from the internet
postconf -e 'inet_protocols = ipv4'
Purpose: Restricts to IPv4 only.
- Simplifies configuration for IPv4-only setups
- Change to
allto support both IPv4 and IPv6 - Many providers don't provide IPv6 or it requires additional setup
postconf -e 'mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain'
Purpose: Defines which domains this server accepts mail for (final destination).
- $myhostname:
mx.itisajoke.net(mail to server itself) - localhost.$mydomain:
localhost.itisajoke.net - localhost: Local machine
- $mydomain:
itisajoke.net(main domain) - Mail to any of these domains is delivered locally, not relayed
postconf -e 'mynetworks = 127.0.0.0/8'
Purpose: Defines trusted networks that can send mail without authentication.
- 127.0.0.0/8: Local machine only
- Prevents open relay (would allow anyone to send mail through your server)
- External users MUST authenticate to send mail
postconf -e 'home_mailbox = Maildir/'
Purpose: Specifies mailbox format and location.
- Maildir/: One-message-per-file format (recommended)
- Alternative:
mbox(single file, prone to corruption) - Stored in user's home:
/home/username/Maildir/ - Trailing
/is important - indicates Maildir format
postconf -e 'smtpd_banner = $myhostname ESMTP'
Purpose: Sets the greeting banner when clients connect.
- ESMTP: Extended SMTP (supports modern features)
- Hides Postfix version number (security through obscurity)
- Default would show:
$myhostname ESMTP Postfix (Ubuntu)
postconf -e 'message_size_limit = 10485760'
Purpose: Maximum message size in bytes.
- 10485760 bytes: 10 MB
- Prevents abuse and disk filling
- Rejects messages larger than this limit
- Includes headers + body + attachments
postconf -e 'mailbox_size_limit = 0'
Purpose: Maximum mailbox size per user.
- 0: Unlimited
- Would set a quota in bytes if non-zero
- Users can accumulate unlimited email storage
- Consider setting a limit for production (e.g., 1GB = 1073741824)
SSL/TLS Configuration
Initial Self-Signed Certificate
CERT_DIR="/etc/letsencrypt/live/$HOSTNAME"
mkdir -p "$CERT_DIR"
Purpose: Creates directory for SSL certificates.
- Uses Let's Encrypt's standard directory structure
- Even though initially self-signed, uses same path
- Let's Encrypt will replace these files later
openssl req -new -x509 -days 365 -nodes \
-out "$CERT_DIR/fullchain.pem" \
-keyout "$CERT_DIR/privkey.pem" \
-subj "/C=US/ST=State/L=City/O=Organization/CN=$HOSTNAME"
Purpose: Generates temporary self-signed SSL certificate.
- -x509: Self-signed certificate (not a CSR)
- -days 365: Valid for one year
- -nodes: No password protection on private key
- fullchain.pem: Certificate file
- privkey.pem: Private key file
- CN=$HOSTNAME: Certificate matches hostname
- Allows encrypted connections immediately
- Replaced with Let's Encrypt certificate after DNS setup
Postfix TLS Configuration
postconf -e "smtpd_tls_cert_file = $CERT_DIR/fullchain.pem"
postconf -e "smtpd_tls_key_file = $CERT_DIR/privkey.pem"
Purpose: Tells Postfix where SSL certificate and key are located.
- smtpd_tls_cert_file: Public certificate
- smtpd_tls_key_file: Private key
- Used for STARTTLS on port 25 and 587
postconf -e 'smtpd_tls_security_level = may'
Purpose: Enables opportunistic TLS encryption.
- may: Offers TLS but doesn't require it
- encrypt: Would require TLS (breaks mail from servers without TLS)
- none: Would disable TLS
- Best balance between security and compatibility
postconf -e 'smtpd_tls_auth_only = yes'
Purpose: Requires TLS encryption for SASL authentication.
- Authentication credentials sent only over encrypted connections
- Prevents password sniffing
- Combined with
smtpd_tls_security_level = may:- TLS is optional for receiving mail
- TLS is required for authenticated sending
postconf -e 'smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1'
Purpose: Disables insecure TLS versions.
- !: Negation (disable)
- SSLv2, SSLv3: Ancient, completely broken
- TLSv1, TLSv1.1: Deprecated, have known vulnerabilities
- Only allows TLSv1.2 and TLSv1.3 (secure modern protocols)
postconf -e 'smtpd_tls_ciphers = high'
Purpose: Requires strong encryption ciphers.
- high: Strong ciphers only (AES, etc.)
- medium: Would allow weaker ciphers
- export: Would allow very weak export-grade ciphers
- Prevents downgrade attacks
postconf -e 'smtp_tls_security_level = may'
Purpose: Enables TLS for outgoing mail (when possible).
- smtp (not smtpd): Outgoing connections
- may: Use TLS if recipient server supports it
- Encrypts mail in transit when possible
- Doesn't fail if recipient doesn't support TLS
postconf -e 'smtpd_tls_loglevel = 1'
Purpose: Logs TLS connection information.
- 0: No TLS logging
- 1: Log TLS handshake summary
- 2+: Very verbose (debug only)
- Helps troubleshoot TLS connection issues
Anti-Spam Measures
HELO Restrictions
postconf -e 'smtpd_helo_required = yes'
Purpose: Requires HELO/EHLO command from connecting clients.
- HELO: SMTP greeting command
- Spammers often skip HELO to save time
- Legitimate mail servers always send HELO
- Rejects connections without proper SMTP greeting
postconf -e 'smtpd_helo_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname, reject_unknown_helo_hostname, permit'
Purpose: Validates HELO/EHLO hostname.
Rules applied in order:
- permit_mynetworks: Allow local connections (127.0.0.0/8)
- permit_sasl_authenticated: Allow authenticated users
- reject_invalid_helo_hostname: Reject malformed hostnames
- reject_non_fqdn_helo_hostname: Reject non-FQDN (e.g., "localhost")
- reject_unknown_helo_hostname: Reject if HELO hostname has no DNS record
- permit: Allow everything else
Blocks spammers using fake or invalid HELO hostnames.
Sender Restrictions
postconf -e 'smtpd_sender_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_non_fqdn_sender, reject_unknown_sender_domain, permit'
Purpose: Validates sender (FROM) address.
Rules:
- permit_mynetworks: Allow local senders
- permit_sasl_authenticated: Allow authenticated users
- reject_non_fqdn_sender: Reject if sender isn't fully qualified (e.g., "user" instead of "user@domain.com")
- reject_unknown_sender_domain: Reject if sender domain has no MX/A record
- permit: Allow everything else
Prevents spoofed sender addresses from non-existent domains.
Recipient Restrictions
postconf -e 'smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_non_fqdn_recipient, reject_unknown_recipient_domain, reject_unauth_destination, reject_rbl_client zen.spamhaus.org, reject_rbl_client bl.spamcop.net, permit'
Purpose: Most important anti-spam layer - validates recipients and checks blacklists.
Rules:
- permit_mynetworks: Allow local delivery
- permit_sasl_authenticated: Allow authenticated users to send anywhere
- reject_non_fqdn_recipient: Reject malformed recipient addresses
- reject_unknown_recipient_domain: Reject if recipient domain doesn't exist
- reject_unauth_destination: CRITICAL - Prevents open relay (only accept mail for domains in mydestination)
- reject_rbl_client zen.spamhaus.org: Check Spamhaus blacklist
- reject_rbl_client bl.spamcop.net: Check SpamCop blacklist
- permit: Allow if all checks pass
Without reject_unauth_destination, server would be an open relay (anyone could send mail through it).
Relay Restrictions
postconf -e 'smtpd_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination'
Purpose: Controls who can relay mail through this server.
Rules:
- permit_mynetworks: Local network can relay
- permit_sasl_authenticated: Authenticated users can relay
- reject_unauth_destination: Reject all other relay attempts
Simplified version of recipient_restrictions focused solely on relay control.
Rate Limiting
postconf -e 'smtpd_client_connection_count_limit = 10'
Purpose: Maximum simultaneous connections per client IP.
- 10: One IP can have 10 concurrent SMTP connections
- Prevents connection flooding
- Legitimate servers rarely need more than 2-3
- Slows down spam zombies
postconf -e 'smtpd_client_connection_rate_limit = 30'
Purpose: Maximum new connections per time unit per client.
- 30: One IP can make 30 new connections per minute
- Blocks rapid-fire connection attempts
- Legitimate use rarely exceeds this
- Stops brute force attacks
postconf -e 'smtpd_client_message_rate_limit = 100'
Purpose: Maximum messages per time unit per client.
- 100: One client can send 100 messages per minute
- Prevents spam floods
- Normal users send 1-10 messages per minute
- Allows bulk sending but prevents abuse
SASL Authentication
Dovecot SASL Integration
postconf -e 'smtpd_sasl_type = dovecot'
Purpose: Use Dovecot for SMTP authentication instead of Cyrus SASL.
- dovecot: Modern, integrated with Dovecot
- cyrus: Older, separate daemon
- Single authentication system for IMAP and SMTP
- Simpler configuration
postconf -e 'smtpd_sasl_path = private/auth'
Purpose: Unix socket path for Dovecot authentication.
- private/auth: Relative to Postfix chroot
/var/spool/postfix/ - Full path:
/var/spool/postfix/private/auth - Dovecot creates this socket
- Postfix connects to it for authentication
postconf -e 'smtpd_sasl_auth_enable = yes'
Purpose: Enables SASL authentication.
- Allows clients to authenticate with username/password
- Required for users to send mail through server
- Without this, only local users can send
postconf -e 'smtpd_sasl_security_options = noanonymous'
Purpose: Disables anonymous authentication.
- noanonymous: Requires username/password
- noplaintext: Would disable plain passwords (too strict for most)
- Prevents unauthenticated sending
postconf -e 'broken_sasl_auth_clients = yes'
Purpose: Compatibility for older email clients.
- Some old clients (Outlook Express, old Thunderbird) use non-standard AUTH
- Enables compatibility modes
- Harmless to enable, helps ancient clients
Submission Port (587)
Modern email clients should use port 587 (submission) instead of port 25 (SMTP) for sending mail.
postconf -M submission/inet="submission inet n - y - - smtpd"
Purpose: Enables submission service on port 587.
- submission: Service name
- inet: Network service
- n: No privileged mode
- -: No chroot (uses default)
- y: Unpriv (can run as non-root)
- -: No wakeup time
- -: No process limit
- smtpd: Use smtpd daemon
postconf -P "submission/inet/syslog_name=postfix/submission"
Purpose: Separate log prefix for submission port.
- Logs show
postfix/submissioninstead ofpostfix/smtp - Easier to distinguish port 25 vs 587 traffic in logs
postconf -P "submission/inet/smtpd_tls_security_level=encrypt"
Purpose: Requires TLS encryption on port 587.
- encrypt: Mandatory TLS (vs "may" which is optional)
- Port 587 is submission port - should always use TLS
- Protects authentication credentials
postconf -P "submission/inet/smtpd_sasl_auth_enable=yes"
Purpose: Enables authentication on submission port.
- Users must authenticate to send mail
- Prevents unauthorized relay
postconf -P "submission/inet/smtpd_tls_auth_only=yes"
Purpose: Requires TLS before allowing authentication.
- Prevents sending passwords in cleartext
- Combined with
smtpd_tls_security_level=encrypt - Double protection for credentials
postconf -P "submission/inet/smtpd_recipient_restrictions=permit_sasl_authenticated,reject"
Purpose: Only authenticated users can send mail.
- permit_sasl_authenticated: If authenticated, allow
- reject: Reject everything else
- Simpler than main recipient_restrictions
- Port 587 is only for authenticated sending
postconf -P "submission/inet/smtpd_relay_restrictions=permit_sasl_authenticated,reject"
Purpose: Relay control for submission port.
- Mirrors recipient_restrictions
- Authenticated users can relay
- Non-authenticated cannot
postconf -P "submission/inet/milter_macro_daemon_name=ORIGINATING"
Purpose: Marks mail from port 587 as originating (not relayed).
- ORIGINATING: This mail is from our users
- Affects DKIM signing behavior
- Some filters treat originating mail differently
- Helps reputation systems
Dovecot IMAP/POP3 Server
Authentication Settings
sed -i 's/^#disable_plaintext_auth = yes/disable_plaintext_auth = yes/' /etc/dovecot/conf.d/10-auth.conf
Purpose: Disables plaintext authentication over unencrypted connections.
- disable_plaintext_auth = yes: Requires TLS for authentication
- Prevents password sniffing
- Uncomments the setting (removes #)
sed -i 's/^auth_mechanisms = plain$/auth_mechanisms = plain login/' /etc/dovecot/conf.d/10-auth.conf
Purpose: Enables both PLAIN and LOGIN authentication mechanisms.
- plain: Standard SASL PLAIN (username + password in one command)
- login: Older mechanism (username and password separate)
- LOGIN needed for old email clients (Outlook, old Android)
SSL/TLS Configuration
sed -i 's|^ssl = yes|ssl = required|' /etc/dovecot/conf.d/10-ssl.conf
Purpose: Requires SSL/TLS for all connections.
- required: No plaintext connections allowed
- yes: Would allow plaintext connections too
- Forces clients to use IMAPS (993) or POP3S (995)
sed -i "s|^ssl_cert = <.*|ssl_cert = <$CERT_DIR/fullchain.pem|" /etc/dovecot/conf.d/10-ssl.conf
sed -i "s|^ssl_key = <.*|ssl_key = <$CERT_DIR/privkey.pem|" /etc/dovecot/conf.d/10-ssl.conf
Purpose: Points Dovecot to SSL certificate files.
- ssl_cert: Public certificate
- ssl_key: Private key
- <: Indicates file content (Dovecot syntax)
- Uses same certificate as Postfix
Mailbox Location
sed -i 's|^mail_location = .*|mail_location = maildir:~/Maildir|' /etc/dovecot/conf.d/10-mail.conf
Purpose: Specifies mailbox format and location.
- maildir: Maildir format (one file per message)
- ~/Maildir: In user's home directory
- Must match Postfix's
home_mailboxsetting - Alternative would be
mbox:~/mailfor mbox format
Postfix Integration
cat >> /etc/dovecot/conf.d/10-master.conf << 'EOF'
service auth {
unix_listener /var/spool/postfix/private/auth {
mode = 0660
user = postfix
group = postfix
}
}
EOF
Purpose: Creates authentication socket for Postfix.
- unix_listener: Creates Unix domain socket
- /var/spool/postfix/private/auth: Same path Postfix expects
- mode = 0660: Read/write for owner and group
- user = postfix, group = postfix: Postfix can access socket
- Allows Postfix to authenticate SMTP users through Dovecot
Certificate Directory Permissions
chmod 755 /etc/letsencrypt/{live,archive}
Purpose: Allows Dovecot to read Let's Encrypt certificates.
- 755: rwxr-xr-x (owner full, others read+execute)
- By default, Let's Encrypt directories are 700 (owner only)
- Dovecot runs as different user, needs read access
- Only affects directories, not private keys (which stay 600)
OpenDKIM Email Signing
DKIM (DomainKeys Identified Mail) cryptographically signs outgoing emails to prove they came from your server.
Directory Setup
mkdir -p /etc/opendkim/keys/$DOMAIN
chown -R opendkim:opendkim /etc/opendkim
chmod 700 /etc/opendkim/keys
Purpose: Creates directories for DKIM configuration and keys.
- /etc/opendkim/keys/$DOMAIN: Stores private signing keys per domain
- opendkim:opendkim: Owned by opendkim daemon
- 700: Only opendkim user can access (private keys are sensitive)
Main Configuration File
cat > /etc/opendkim.conf << 'EOF'
Syslog yes
SyslogSuccess yes
Canonicalization relaxed/simple
Mode sv
SubDomains no
AutoRestart yes
SignatureAlgorithm rsa-sha256
UserID opendkim
Socket local:/var/spool/postfix/opendkim/opendkim.sock
PidFile /run/opendkim/opendkim.pid
KeyTable refile:/etc/opendkim/key.table
SigningTable refile:/etc/opendkim/signing.table
ExternalIgnoreList /etc/opendkim/trusted.hosts
InternalHosts /etc/opendkim/trusted.hosts
EOF
Configuration explained:
- Syslog yes: Log to syslog
- SyslogSuccess yes: Log successful signatures (helps debugging)
- Canonicalization relaxed/simple:
- relaxed: Allows whitespace changes in headers
- simple: Body must be exact
- Balance between compatibility and security
- Mode sv:
- s: Sign outgoing mail
- v: Verify incoming mail signatures
- SubDomains no: Don't sign for subdomains
- AutoRestart yes: Restart on failure
- SignatureAlgorithm rsa-sha256: Modern, secure algorithm
- UserID opendkim: Run as opendkim user
- Socket: Unix socket for Postfix communication
- PidFile: Process ID file location
- KeyTable: Maps selectors to key files
- SigningTable: Maps email addresses to selectors
- ExternalIgnoreList / InternalHosts: Trusted hosts that bypass checks
Key Table
echo "$PREFIX._domainkey.$DOMAIN $DOMAIN:$PREFIX:/etc/opendkim/keys/$DOMAIN/$PREFIX.private" > /etc/opendkim/key.table
Purpose: Maps DKIM selectors to private key files.
Format: selector domain:selector:keyfile
Example: mx._domainkey.itisajoke.net itisajoke.net:mx:/etc/opendkim/keys/itisajoke.net/mx.private
- mx._domainkey.itisajoke.net: DNS record name
- itisajoke.net:mx: Domain and selector
- keyfile: Path to private key
Signing Table
echo "*@$DOMAIN $PREFIX._domainkey.$DOMAIN" > /etc/opendkim/signing.table
Purpose: Maps sender addresses to DKIM keys.
Format: pattern selector
Example: \1*\2itisajoke.net mx._domainkey.itisajoke.net`
- \1*\2itisajoke.net: All addresses from this domain
- mx._domainkey.itisajoke.net: Use this key to sign
Trusted Hosts
cat > /etc/opendkim/trusted.hosts << EOF
127.0.0.1
localhost
$SERVER_IP
$HOSTNAME
$DOMAIN
*.${DOMAIN}
EOF
Purpose: Lists hosts allowed to send mail without DKIM requirements.
- 127.0.0.1, localhost: Local machine
- $SERVER_IP: This server's IP
- $HOSTNAME: This server's hostname
- $DOMAIN: Main domain
- \1*\2${DOMAIN}: All subdomains
- Mail from these sources is signed but verification isn't enforced
DKIM Key Generation
opendkim-genkey -b 2048 -d "$DOMAIN" -D "/etc/opendkim/keys/$DOMAIN" -s $PREFIX -v
Purpose: Generates RSA key pair for DKIM signing.
- -b 2048: 2048-bit key (secure, widely supported)
- -d "$DOMAIN": Domain name
- -D "path": Output directory
- -s $PREFIX: Selector (e.g., "mx")
- -v: Verbose output
Creates two files:
- mx.private: Private key (keep secret!)
- mx.txt: Public key for DNS
chown -R opendkim:opendkim /etc/opendkim
chmod 600 /etc/opendkim/keys/$DOMAIN/$PREFIX.private
Purpose: Secures private key.
- opendkim:opendkim: Only opendkim can access
- 600: Owner can read/write, nobody else can access
- Private key must be kept secret
DKIM Public Key Extraction
DKIM_KEY_FILE="/etc/opendkim/keys/$DOMAIN/$PREFIX.txt"
DKIM_PUBLIC_KEY=$(grep '"' "$DKIM_KEY_FILE" | tr -d '\n\t "()' | sed -n 's/.*p=\([A-Za-z0-9+\/=]*\).*/\1/p')
echo "v=DKIM1; k=rsa; p=$DKIM_PUBLIC_KEY" > /root/dkim-public-key.txt
Purpose: Extracts public key for DNS record.
- grep '"': Gets lines with quotes (the key data)
- tr -d '\n\t "()': Removes formatting
- sed: Extracts base64 key after p=
- v=DKIM1: DKIM version
- k=rsa: RSA key type
- p=: Public key data
Output file /root/dkim-public-key.txt contains exactly what goes in DNS TXT record.
Postfix Milter Integration
mkdir -p /var/spool/postfix/opendkim
chown opendkim:postfix /var/spool/postfix/opendkim
chmod 750 /var/spool/postfix/opendkim
Purpose: Creates directory for OpenDKIM socket inside Postfix chroot.
- opendkim:postfix: opendkim creates, postfix reads
- 750: rwxr-x--- (owner full, group read+execute)
- Inside Postfix chroot so Postfix can access it
OpenDKIM Service Configuration
mkdir -p /etc/systemd/system/opendkim.service.d
cat > /etc/systemd/system/opendkim.service.d/override.conf << 'EOF'
[Service]
ExecStartPre=/bin/mkdir -p /var/spool/postfix/opendkim
ExecStartPre=/bin/chown opendkim:postfix /var/spool/postfix/opendkim
ExecStartPre=/bin/chmod 750 /var/spool/postfix/opendkim
EOF
Purpose: Ensures socket directory exists before OpenDKIM starts.
- ExecStartPre: Runs before service starts
- Creates directory if missing (after reboot)
- Sets correct permissions
- systemd override: Doesn't modify main service file
Postfix Milter Configuration
postconf -e 'milter_default_action = accept'
Purpose: What to do if milter fails.
- accept: Deliver mail even if DKIM signing fails
- reject: Would reject mail if signing fails (too strict)
- tempfail: Would defer mail (retry later)
- Prevents mail loss if OpenDKIM crashes
postconf -e 'milter_protocol = 6'
Purpose: Milter protocol version.
- 6: Modern protocol version
- Required for OpenDKIM
- Older versions (2, 3) lack features
postconf -e 'smtpd_milters = local:opendkim/opendkim.sock'
Purpose: Milters for incoming mail (SMTP daemon).
- local:: Unix socket (not network)
- opendkim/opendkim.sock: Relative to Postfix chroot
- Full path:
/var/spool/postfix/opendkim/opendkim.sock - Processes all incoming SMTP connections
postconf -e 'non_smtpd_milters = $smtpd_milters'
Purpose: Milters for mail injected locally (not via SMTP).
- $smtpd_milters: Use same milters as SMTP
- Covers mail from
sendmailcommand - Ensures all outgoing mail is signed
Email Aliases
grep -q "^postmaster:" /etc/aliases || echo "postmaster: root" >> /etc/aliases
grep -q "^abuse:" /etc/aliases || echo "abuse: root" >> /etc/aliases
Purpose: Creates required email aliases.
- postmaster@domain: Required by RFC 5321 (mail admin contact)
- abuse@domain: Spam/abuse reports
- Both redirect to root account
- grep -q: Only add if not already present
sed -i '/^root:/d' /etc/aliases
echo "root: $SSL_EMAIL" >> /etc/aliases
Purpose: Redirects root's mail to real email address.
- sed -i '/^root:/d': Removes any existing root alias
- echo: Adds new root alias
- System messages (cron, security) sent to real address
- Ensures you get server notifications
newaliases
Purpose: Rebuilds alias database.
- Converts
/etc/aliasestext file to binary database - Postfix reads binary format for performance
- Must run after any alias changes
User Creation
if ! id "$TEST_USERNAME" &>/dev/null; then
useradd -m -s /bin/bash "$TEST_USERNAME"
echo "$TEST_USERNAME:$TEST_PASSWORD" | chpasswd
mkdir -p /home/$TEST_USERNAME/Maildir/{new,cur,tmp}
mkdir -p /home/$TEST_USERNAME/Maildir/.{Sent,Trash,Drafts}/{new,cur,tmp}
chown -R $TEST_USERNAME:$TEST_USERNAME /home/$TEST_USERNAME/Maildir
chmod -R 700 /home/$TEST_USERNAME/Maildir
fi
Purpose: Creates initial mail user with Maildir structure.
Steps:
- id "$TEST_USERNAME": Check if user exists
- useradd -m -s /bin/bash: Create user with home directory and bash shell
- chpasswd: Set password
- mkdir Maildir: Create Maildir structure
- new/: New unread messages
- cur/: Current (read) messages
- tmp/: Temporary storage during delivery
- Subdirectories: Special folders (Sent, Trash, Drafts)
- .Sent: Period prefix indicates subfolder
- Each has new/cur/tmp structure
- chown: Make user own maildir
- chmod 700: Only user can access their mail
Firewall Configuration
if command -v ufw &> /dev/null; then
ufw allow 22/tcp comment 'SSH'
ufw allow 25/tcp comment 'SMTP'
ufw allow 80/tcp comment 'HTTP'
ufw allow 587/tcp comment 'Submission'
ufw allow 993/tcp comment 'IMAPS'
ufw allow 995/tcp comment 'POP3S'
ufw --force enable
fi
Purpose: Opens required firewall ports.
Ports:
- 22/tcp: SSH (remote administration)
- 25/tcp: SMTP (incoming mail from other servers)
- 80/tcp: HTTP (Let's Encrypt certificate validation)
- 587/tcp: Submission (authenticated mail sending)
- 993/tcp: IMAPS (IMAP over SSL/TLS)
- 995/tcp: POP3S (POP3 over SSL/TLS)
Note: Unencrypted ports (110, 143) are NOT opened because ssl = required in Dovecot.
--force enable: Enables firewall without prompting (automated install)
Service Startup
systemctl daemon-reload
Purpose: Reload systemd configuration.
- Reads new/modified service files
- Picks up OpenDKIM override configuration
- Required after creating override.conf
systemctl restart opendkim && systemctl enable opendkim
sleep 3
Purpose: Start OpenDKIM and enable on boot.
- restart: Start (or restart if running)
- enable: Start automatically on server boot
- sleep 3: Wait for socket creation
chown opendkim:postfix /var/spool/postfix/opendkim/opendkim.sock
chmod 660 /var/spool/postfix/opendkim/opendkim.sock
Purpose: Fix socket permissions (critical for DKIM signing).
- Default: opendkim:opendkim with 755
- Need: opendkim:postfix with 660
- 660: rw-rw---- (owner and group can read/write)
- postfix group: Allows Postfix to write to socket
- Without this, emails won't be DKIM signed
systemctl restart postfix && systemctl enable postfix
systemctl restart dovecot && systemctl enable dovecot
Purpose: Start mail services and enable on boot.
- All configuration loaded
- Services will auto-start on reboot
sleep 3
Purpose: Wait for services to fully start.
- Services may take a moment to bind ports
- Ensures they're ready before script continues
Helper Scripts
Add Mail User Script
cat > /root/add-mail-user.sh << 'EOFSCRIPT'
#!/bin/bash
[ $# -ne 2 ] && { echo "Usage: $0 <username> <password>"; exit 1; }
USERNAME=$1; PASSWORD=$2; DOMAIN=$(postconf -h mydomain)
id "$USERNAME" &>/dev/null && { echo "User exists"; exit 1; }
useradd -m -s /bin/bash "$USERNAME"
echo "$USERNAME:$PASSWORD" | chpasswd
mkdir -p /home/$USERNAME/Maildir/{new,cur,tmp}
mkdir -p /home/$USERNAME/Maildir/.{Sent,Trash,Drafts}/{new,cur,tmp}
chown -R $USERNAME:$USERNAME /home/$USERNAME/Maildir
chmod -R 700 /home/$USERNAME/Maildir
echo "Created: $USERNAME@$DOMAIN"
EOFSCRIPT
Purpose: Simplifies adding new mail users.
Usage: /root/add-mail-user.sh john SecurePassword123
Steps:
- Check for 2 arguments (username, password)
- Get domain from Postfix config
- Check user doesn't already exist
- Create system user
- Set password
- Create Maildir structure
- Set ownership and permissions
- Confirm creation
Test Mail Script
cat > /root/test-mail.sh << 'EOFSCRIPT'
#!/bin/bash
DOMAIN=$(postconf -h mydomain)
HOSTNAME=$(postconf -h myhostname)
echo "Mail Server: $HOSTNAME"
systemctl is-active postfix && echo " [OK] Postfix" || echo " [FAIL] Postfix"
systemctl is-active dovecot && echo " [OK] Dovecot" || echo " [FAIL] Dovecot"
systemctl is-active opendkim && echo " [OK] OpenDKIM" || echo " [FAIL] OpenDKIM"
[ $# -eq 1 ] && {
echo "Sending test to $1..."
echo "Test from $HOSTNAME at $(date)" | mail -s "Mail Test $(date +%s)" $1
echo "Check: tail -f /var/log/mail.log"
} || echo "Usage: $0 <recipient@example.com>"
EOFSCRIPT
Purpose: Tests mail server functionality.
Usage: /root/test-mail.sh test@mail-tester.com
Features:
- Shows server hostname
- Checks service status (Postfix, Dovecot, OpenDKIM)
- Sends test email if recipient provided
- Suggests checking mail log
DNS Verification Script
cat > /root/verify-dns.sh << 'EOFSCRIPT'
#!/bin/bash
DOMAIN=$(postconf -h mydomain)
HOSTNAME=$(postconf -h myhostname)
SERVER_IP=$(ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '127.0.0.1' | head -n1)
# Extract prefix from hostname (e.g., "mx" from "mx.itisajoke.net")
PREFIX="${HOSTNAME%%.*}"
echo "DNS VERIFICATION"
echo "=================="
A_RECORD=$(dig @1.1.1.1 +short "$HOSTNAME" | head -n1)
[ "$A_RECORD" == "$SERVER_IP" ] && echo "[OK] A: $HOSTNAME -> $A_RECORD" || echo "[FAIL] A record missing"
MX_RECORD=$(dig @1.1.1.1 +short MX "$DOMAIN" | sort -n | head -n1 | awk '{print $2}')
[[ "$MX_RECORD" == *"$PREFIX.$DOMAIN"* ]] && echo "[OK] MX: $MX_RECORD" || echo "[FAIL] MX missing"
SPF_RECORD=$(dig @1.1.1.1 +short TXT "$DOMAIN" | grep "v=spf1")
[ -n "$SPF_RECORD" ] && echo "[OK] SPF found" || echo "[FAIL] SPF missing"
DKIM_RECORD=$(dig @1.1.1.1 +short TXT "$PREFIX._domainkey.$DOMAIN" | grep "v=DKIM1")
[ -n "$DKIM_RECORD" ] && echo "[OK] DKIM found" || echo "[FAIL] DKIM missing"
DMARC_RECORD=$(dig @1.1.1.1 +short TXT "_dmarc.$DOMAIN" | grep "v=DMARC1")
[ -n "$DMARC_RECORD" ] && echo "[OK] DMARC found" || echo "[FAIL] DMARC missing"
PTR_RECORD=$(dig @1.1.1.1 +short -x "$SERVER_IP" | head -n1)
if [ "$PTR_RECORD" == "$HOSTNAME." ]; then
echo "[OK] PTR: $SERVER_IP -> $PTR_RECORD"
else
echo "[FAIL] PTR: $SERVER_IP -> $PTR_RECORD (should be $HOSTNAME.)"
fi
EOFSCRIPT
Purpose: Verifies all required DNS records are configured.
Usage: /root/verify-dns.sh
Checks:
- A Record: Hostname resolves to server IP
- MX Record: Domain has mail server configured
- SPF Record: Sender Policy Framework configured
- DKIM Record: Public key published
- DMARC Record: Email authentication policy configured
- PTR Record: Reverse DNS configured
Uses @1.1.1.1 (Cloudflare DNS) to avoid local caching issues.
Make Scripts Executable
chmod +x /root/{add-mail-user.sh,test-mail.sh,verify-dns.sh}
Purpose: Makes helper scripts executable.
- chmod +x: Add execute permission
- Allows running scripts directly:
/root/test-mail.sh - Without this, would need:
bash /root/test-mail.sh
DNS Configuration Timer
echo ""
echo "DKIM key:"
cat /root/dkim-public-key.txt
echo ""
echo "Update DNS, then waiting 10 minutes..."
echo ""
# 10-minute countdown
for i in {600..1}; do
printf "\rTime remaining: %02d:%02d" $((i/60)) $((i%60))
sleep 1
done
Purpose: Displays DKIM key and waits for DNS propagation.
Countdown timer:
- {600..1}: Counts from 600 to 1 (10 minutes)
- printf \r: Overwrites same line (live countdown)
- $((i/60)): Minutes remaining
- $((i%60)): Seconds remaining
- User sees live countdown while DNS propagates
What to do during wait: Update DNS with:
- A record for mail server hostname
- MX record pointing to mail server
- SPF TXT record
- DKIM TXT record (shown in output)
- DMARC TXT record
- PTR record (in Linode dashboard)
SSL Certificate Acquisition
CERTBOT_CMD="certbot certonly --standalone --non-interactive --agree-tos --email $SSL_EMAIL -d $HOSTNAME --preferred-challenges http --force-renewal"
$CERTBOT_CMD >> "$LOGFILE" 2>&1 && {
log "SSL certificate obtained"
systemctl restart postfix dovecot
} || {
log "SSL certificate failed - check $LOGFILE for details"
}
Purpose: Obtains Let's Encrypt SSL certificate after DNS is ready.
Certbot options:
- certonly: Only obtain certificate (don't install)
- --standalone: Use built-in web server for validation
- --non-interactive: No prompts
- --agree-tos: Accept Let's Encrypt terms
- --email $SSL_EMAIL: Contact email for expiration notices
- -d $HOSTNAME: Domain to get certificate for
- --preferred-challenges http: Use HTTP-01 validation (port 80)
- --force-renewal: Overwrite any existing certificate
HTTP-01 Challenge:
- Certbot starts temporary web server on port 80
- Let's Encrypt connects to http://mx.itisajoke.net/.well-known/acme-challenge/xxx
- Server responds with validation token
- Let's Encrypt verifies domain ownership
- Certificate issued
Why it might fail:
- DNS not propagated yet (A record missing)
- Port 80 blocked by firewall
- Hostname doesn't resolve
- Rate limit (5 certs per domain per week)
On success:
- Certificate:
/etc/letsencrypt/live/$HOSTNAME/fullchain.pem - Private key:
/etc/letsencrypt/live/$HOSTNAME/privkey.pem - Postfix and Dovecot restarted to use new certificate
- Auto-renewal configured by certbot
On failure:
- Server keeps using self-signed certificate
- Mail works but clients see certificate warning
- Can manually run certbot later
Final Verification
/root/verify-dns.sh
Purpose: Automatically verifies DNS configuration after certificate acquisition.
- Runs DNS checks
- Shows which records are configured correctly
- Helps identify any missing DNS records
- Output visible in script logs and console
Post-Deployment
Email Client Configuration
Mozilla Thunderbird
Step 1: Add New Account
- Open Thunderbird
- Click ☰ (menu) → New → Existing Mail Account
Step 2: Enter Account Details
Your full name: Tom Vitkovic
Email address: tomislav.vitkovic@itisajoke.net
Password: [your password from stackscript_data]
Click Continue
Step 3: Manual Configuration Thunderbird will try auto-detection. Click Manual config button.
Incoming Server (IMAP):
Protocol: IMAP
Hostname: mx.itisajoke.net
Port: 993
Connection security: SSL/TLS
Authentication: Normal password
Username: tomislav.vitkovic
Outgoing Server (SMTP):
Hostname: mx.itisajoke.net
Port: 587
Connection security: STARTTLS
Authentication: Normal password
Username: tomislav.vitkovic
Step 4: Advanced Settings (Optional)
After account creation, you can adjust:
- Right-click account → Settings
- Server Settings → Check mail every X minutes
- Copies & Folders → Where to save sent mail
- Composition & Addressing → Default From address
Common Issues:
- If you get certificate warning, click Confirm Security Exception (Let's Encrypt certificates are valid)
- If authentication fails, verify username is just
tomislav.vitkovic(not full email address)
Apple Mail (macOS)
Step 1: Add Account
- Open Mail app
- Click Mail → Add Account
- Select Other Mail Account → Continue
Step 2: Account Information
Name: Tom Vitkovic
Email Address: tomislav.vitkovic@itisajoke.net
Password: [your password from stackscript_data]
Click Sign In
Step 3: Manual Setup
If auto-detection fails, you'll see incoming/outgoing server fields:
Incoming Mail Server (IMAP):
IMAP Hostname: mx.itisajoke.net
Username: tomislav.vitkovic
Password: [your password]
Click Sign In, then configure outgoing:
Outgoing Mail Server (SMTP):
SMTP Hostname: mx.itisajoke.net
Username: tomislav.vitkovic
Password: [your password]
Step 4: Verify Settings
After setup, verify configuration:
- Mail → Preferences → Accounts
- Select your account
- Click Server Settings
Incoming (IMAP) should show:
Host Name: mx.itisajoke.net
Port: 993
TLS/SSL: ON
Authentication: Password
Outgoing (SMTP) should show:
Host Name: mx.itisajoke.net
Port: 587
TLS/SSL: ON
Authentication: Password
Step 5: Advanced Options
Click Advanced tab:
- Enable this account: Checked
- Include when automatically checking for new messages: Checked
- Remove copy from server after retrieving: Optional (uncheck to keep mail on server)
iOS Mail (iPhone/iPad)
Step 1: Add Account
- Open Settings
- Scroll to Mail
- Tap Accounts → Add Account
- Select Other
- Tap Add Mail Account
Step 2: Enter Information
Name: Tom Vitkovic
Email: tomislav.vitkovic@itisajoke.net
Password: [your password]
Description: My Mail Server
Tap Next
Step 3: Configure Servers
Incoming Mail Server:
Host Name: mx.itisajoke.net
Username: tomislav.vitkovic
Password: [your password]
Outgoing Mail Server:
Host Name: mx.itisajoke.net
Username: tomislav.vitkovic
Password: [your password]
Tap Next, then Save
iOS will automatically detect:
- IMAP port 993 with SSL
- SMTP port 587 with STARTTLS
Using Helper Scripts
The setup script creates three utility scripts in /root/ for server management.
1. Adding New Mail Users
Script: /root/add-mail-user.sh
Purpose: Creates new email accounts on the server
Usage:
/root/add-mail-user.sh <username> <password>
Example:
/root/add-mail-user.sh john MySecurePass123
Output:
Created: john@itisajoke.net
What it does:
- Validates you provided both username and password
- Checks if username already exists (prevents duplicates)
- Creates Linux system user with home directory
- Sets the password
- Creates complete Maildir structure:
/home/john/Maildir/new/- New unread messages/home/john/Maildir/cur/- Current (read) messages/home/john/Maildir/tmp/- Temporary delivery storage/home/john/Maildir/.Sent/- Sent folder/home/john/Maildir/.Trash/- Deleted messages/home/john/Maildir/.Drafts/- Draft messages
- Sets correct ownership (user owns their maildir)
- Sets permissions (700 - only user can access)
User can now:
- Log in via IMAP:
john@itisajoke.net - Send mail via SMTP:
john@itisajoke.net - Use password:
MySecurePass123
Password Requirements:
- Use strong passwords in production
- Minimum 12 characters recommended
- Mix of letters, numbers, symbols
- Avoid dictionary words
Viewing All Mail Users:
ls -la /home/
Any directory except the test user is a mail account.
Deleting a Mail User:
userdel -r username
This removes the user and their entire maildir (cannot be undone).
2. Testing Mail Functionality
Script: /root/test-mail.sh
Purpose: Verifies mail server is working and tests deliverability
Usage:
/root/test-mail.sh [recipient-email]
Example 1: Check Service Status
/root/test-mail.sh
Output:
Mail Server: mx.itisajoke.net
[OK] Postfix
[OK] Dovecot
[OK] OpenDKIM
Usage: /root/test-mail.sh <recipient@example.com>
Shows which mail services are running:
- Postfix: SMTP server (sending/receiving)
- Dovecot: IMAP/POP3 server (mailbox access)
- OpenDKIM: Email signing (authentication)
[OK] = Service running correctly [FAIL] = Service stopped or crashed
Example 2: Send Test Email
/root/test-mail.sh test@mail-tester.com
Output:
Mail Server: mx.itisajoke.net
[OK] Postfix
[OK] Dovecot
[OK] OpenDKIM
Sending test to test@mail-tester.com...
Check: tail -f /var/log/mail.log
What it does:
- Checks service status
- Sends test email with:
- From: Current user (root or actual user)
- To: Address you specified
- Subject: "Mail Test [timestamp]"
- Body: "Test from mx.itisajoke.net at [date/time]"
- Email is DKIM signed automatically
- Shows log command to watch delivery
Testing with mail-tester.com:
/root/test-mail.sh test@mail-tester.com
Then visit: https://www.mail-tester.com
You'll get a score out of 10:
- 10/10: Perfect configuration
- 9/10: Excellent (minor issue, likely blacklist)
- 8/10: Good (needs tuning)
- <7/10: Problems need fixing
Common Issues:
- Score drops if sent from root (use actual user:
su - tomislav.vitkovic) - New servers may be on minor blacklists (auto-resolve in 1-2 weeks)
- Empty message body reduces score (include real content)
Watching Email Delivery:
tail -f /var/log/mail.log
Look for:
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=itisajoke.net
status=sent (250 2.0.0 OK)
DKIM-Signature = Email was signed status=sent = Successfully delivered
3. Verifying DNS Configuration
Script: /root/verify-dns.sh
Purpose: Checks all required DNS records are configured correctly
Usage:
/root/verify-dns.sh
Example Output:
DNS VERIFICATION
==================
[OK] A: mx.itisajoke.net -> 172.104.132.225
[OK] MX: mx.itisajoke.net.
[OK] SPF found
[OK] DKIM found
[OK] DMARC found
[OK] PTR: 172.104.132.225 -> mx.itisajoke.net.
What Each Check Means:
1. A Record:
[OK] A: mx.itisajoke.net -> 172.104.132.225
- Verifies hostname resolves to server IP
- Required for: Let's Encrypt, email delivery
- Checked against: Public DNS (1.1.1.1 - Cloudflare)
- [FAIL] means: DNS not configured or not propagated yet
2. MX Record:
[OK] MX: mx.itisajoke.net.
- Verifies domain has mail server configured
- Other servers use this to find where to send mail
- Should point to your mail server hostname
- [FAIL] means: Mail to @itisajoke.net won't be delivered
3. SPF Record:
[OK] SPF found
- Sender Policy Framework - authorizes your IP to send mail
- Format:
v=spf1 ip4:172.104.132.225 a:mx.itisajoke.net -all - Prevents email spoofing
- [FAIL] means: Your emails may go to spam
4. DKIM Record:
[OK] DKIM found
- Public key for email signature verification
- Format:
v=DKIM1; k=rsa; p=MIIBIjAN... - Proves emails actually came from your server
- [FAIL] means: Email authentication fails, likely spam
5. DMARC Record:
[OK] DMARC found
- Email authentication policy
- Format:
v=DMARC1; p=quarantine; rua=mailto:postmaster@itisajoke.net - Tells receivers what to do with failed authentication
- [FAIL] means: No policy, receivers don't know how to handle failures
6. PTR Record (Reverse DNS):
[OK] PTR: 172.104.132.225 -> mx.itisajoke.net.
- IP address points back to hostname
- Most important for deliverability
- Set in Linode dashboard (not DNS provider)
- [FAIL] means: 90% of your emails go to spam
If You See [FAIL]:
A Record Failed:
[FAIL] A record missing
Check your DNS provider has:
Type: A
Name: mx
Value: 172.104.132.225
TTL: 300 (or default)
Wait 5-10 minutes for propagation.
DKIM Failed:
[FAIL] DKIM missing
Check DNS has complete DKIM key:
cat /root/dkim-public-key.txt
Copy entire output to DNS TXT record:
Type: TXT
Name: mx._domainkey
Value: v=DKIM1; k=rsa; p=MIIBIjAN... [full key]
PTR Failed:
[FAIL] PTR: 172.104.132.225 -> 172-104-132-225.ip.linodeusercontent.com. (should be mx.itisajoke.net.)
This means PTR not set in Linode:
- Go to Linode Cloud Manager
- Click your instance
- Network tab
- Find your IP: 172.104.132.225
- Click Edit RDNS
- Enter:
mx.itisajoke.net - Save
Using with Monitoring:
Create a cron job to check DNS daily:
crontab -e
Add:
0 9 * * * /root/verify-dns.sh | mail -s "Daily DNS Check" admin@itisajoke.net
This emails you DNS status every morning at 9 AM.
Quick Reference
Check Service Status:
systemctl status postfix dovecot opendkim
View Mail Logs:
tail -f /var/log/mail.log
Test DKIM Key:
opendkim-testkey -d itisajoke.net -s mx -vvv
Manual Email Send (Command Line):
echo "Test message" | mail -s "Test Subject" recipient@example.com
Check Mail Queue:
mailq
Flush Mail Queue:
postfix flush
View User's Mailbox:
ls -la /home/username/Maildir/new/
Monitor Active Connections:
netstat -tulpn | grep -E ':(25|587|993|995)'
Security Considerations
Password Security:
- Never use test passwords in production
- Minimum 12 characters with mixed case, numbers, symbols
- Change passwords regularly
- Different password for each user
System Updates:
apt-get update && apt-get upgrade -y
Run weekly to get security patches.
Firewall:
- Already configured via UFW
- Only necessary ports open (22, 25, 80, 587, 993, 995)
- Port 22 (SSH) should be restricted to known IPs in production
Monitoring:
# Watch for authentication failures
grep "authentication failed" /var/log/mail.log
# Watch for unusual activity
tail -f /var/log/mail.log
Fail2ban (Optional but Recommended):
apt-get install fail2ban -y
Automatically blocks IPs after repeated failed login attempts.
Backups:
- Regular backups of
/home(user mailboxes) - Regular backups of
/etc/postfix,/etc/dovecot,/etc/opendkim - Store backups off-server
- Test restoration procedure
Maintenance
Certificate Renewal
Automatic renewal configured by certbot:
# Check renewal timer
systemctl status certbot.timer
# Test renewal (dry-run)
certbot renew --dry-run
# Manual renewal
certbot renew
systemctl restart postfix dovecot
Backup Strategy
# Backup mail and configuration
tar -czf /root/mail-backup-$(date +%Y%m%d).tar.gz \
/home \
/etc/postfix \
/etc/dovecot \
/etc/opendkim \
/etc/letsencrypt
# Restore from backup
tar -xzf /root/mail-backup-YYYYMMDD.tar.gz -C /
systemctl restart postfix dovecot opendkim
Log Rotation
Automatic log rotation configured by default:
/var/log/mail.log- Main mail log/var/log/mail.err- Error log- Rotated weekly
- Kept for 4 weeks
- Compressed after rotation
Performance Tuning
For high-volume servers:
# Increase connection limits
postconf -e 'default_process_limit = 200'
postconf -e 'smtpd_client_connection_count_limit = 50'
# Increase worker processes
postconf -e 'smtp_connection_cache_destinations = 1000'
# Optimize Dovecot
# Edit /etc/dovecot/conf.d/10-mail.conf
# mail_fsync = never # Faster but less safe
# Restart services
systemctl restart postfix dovecot