Tor Mail Server

  • Below outlines the steps to take to configure your own mail server.

Install Dependencies

you will also need to install dovecot & postfix.

sudo apt-get install apt-transport-tor gnupg -y
# https://support.torproject.org/apt/tor-deb-repo/
wget -qO- https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc | gpg --dearmor | tee /usr/share/keyrings/deb.torproject.org-keyring.gpg >/dev/null
sudo apt-get install tor -y

Edit Torrc File:

# Transport Port
TransPort 9040

# Auto Resolve
AutomapHostsOnResolve 1

# Daemon
RunAsDaemon 1

# Hardware Acceleration Crypto
HardwareAccel 1

# DNS Safety
ClientDNSRejectInternalAddresses 1
ClientRejectInternalAddresses 1
DNSPort 127.0.0.1:53

# Circuit Hacks
NewCircuitPeriod 40
MaxCircuitDirtiness 600
MaxClientCircuitsPending 48
UseEntryGuards 1
EnforceDistinctSubnets 1

# Cookie
CookieAuthentication 1
CookieAuthFile /var/run/tor/control.authcookie

# SSH
HiddenServiceDir /var/lib/tor/ssh/
HiddenServiceVersion 3
HiddenServicePort 22 127.0.0.1:22

# Mail Server
HiddenServiceDir /var/lib/tor/mailserver/
HiddenServiceVersion 3
#HiddenServicePort 25 127.0.0.1:25
#HiddenServicePort 143 127.0.0.1:143
#HiddenServicePort 587 127.0.0.1:587
#HiddenServicePort 993 127.0.0.1:993

# Postfix
HiddenServicePort 25 127.0.0.1:25     
# SMTP (server-to-server)
HiddenServicePort 587 127.0.0.1:587   
# Submission (SMTP+AUTH+STARTTLS)

# Dovecot
HiddenServicePort 143 127.0.0.1:143   
# IMAP
HiddenServicePort 993 127.0.0.1:993   
# IMAPS
HiddenServicePort 110 127.0.0.1:110   
# POP3
HiddenServicePort 995 127.0.0.1:995   
# POP3S

Modify sources.list.d

debian.list

deb     tor+http://2s4yqjx5ul6okpp3f2gaunr2syex5jgbfpfvhxxbbjwnrsvbk5v3qbid.onion/debian            bookworm         main
deb-src tor+http://2s4yqjx5ul6okpp3f2gaunr2syex5jgbfpfvhxxbbjwnrsvbk5v3qbid.onion/debian            bookworm         main

deb     tor+http://2s4yqjx5ul6okpp3f2gaunr2syex5jgbfpfvhxxbbjwnrsvbk5v3qbid.onion/debian            bookworm-updates main
deb-src tor+http://2s4yqjx5ul6okpp3f2gaunr2syex5jgbfpfvhxxbbjwnrsvbk5v3qbid.onion/debian            bookworm-updates main

deb     tor+http://5ajw6aqf3ep7sijnscdzw77t7xq4xjpsy335yb2wiwgouo7yfxtjlmid.onion/debian-security     bookworm-security main
deb-src tor+http://5ajw6aqf3ep7sijnscdzw77t7xq4xjpsy335yb2wiwgouo7yfxtjlmid.onion/debian-security     bookworm-security main

# Optional backports (may not be populated)
# deb     tor+http://2s4yqjx5ul6okpp3f2gaunr2syex5jgbfpfvhxxbbjwnrsvbk5v3qbid.onion/debian          bookworm-backports main
# deb-src tor+http://2s4yqjx5ul6okpp3f2gaunr2syex5jgbfpfvhxxbbjwnrsvbk5v3qbid.onion/debian          bookworm-backports main

tor.list

deb [signed-by=/usr/share/keyrings/deb.torproject.org-keyring.gpg] tor+http://apow7mjfryruh65chtdydfmqfpj5btws7nbocgtaovhvezgccyjazpqd.onion/torproject.org bookworm main

# For the unstable version.
deb [signed-by=/usr/share/keyrings/deb.torproject.org-keyring.gpg] tor+http://apow7mjfryruh65chtdydfmqfpj5btws7nbocgtaovhvezgccyjazpqd.onion/torproject.org tor-nightly-main-bookworm main


Proxy

route network traffic through tor

#!/bin/bash
# aMiscreant

# Flush & drop all
iptables -F
iptables -P INPUT DROP
iptables -P OUTPUT DROP

# Allow already-established sessions
iptables -A INPUT  -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# Allow localhost
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT

# Tor daemon access
iptables -A OUTPUT -m owner --uid-owner debian-tor -j ACCEPT

# Tor DNS and SOCKS only for debian-tor
iptables -A OUTPUT -p tcp --dport 9040 -m owner --uid-owner debian-tor -j ACCEPT
iptables -A OUTPUT -p tcp --dport 9050 -m owner --uid-owner debian-tor -j ACCEPT
iptables -A OUTPUT -p udp --dport 53   -m owner --uid-owner debian-tor -j ACCEPT
iptables -A OUTPUT -p tcp --dport 53   -m owner --uid-owner debian-tor -j ACCEPT

# ALLOW local-only onion forward ports
iptables -A INPUT -p tcp -s 127.0.0.1 --dport 22  -j ACCEPT  # SSH
iptables -A INPUT -p tcp -s 127.0.0.1 --dport 25  -j ACCEPT  # SMTP
iptables -A INPUT -p tcp -s 127.0.0.1 --dport 587 -j ACCEPT  # SMTP Auth
iptables -A INPUT -p tcp -s 127.0.0.1 --dport 143 -j ACCEPT  # IMAP
iptables -A INPUT -p tcp -s 127.0.0.1 --dport 993 -j ACCEPT  # IMAPS
iptables -A INPUT -p tcp -s 127.0.0.1 --dport 110 -j ACCEPT  # POP3
iptables -A INPUT -p tcp -s 127.0.0.1 --dport 995 -j ACCEPT  # POP3S

Postfix & Dovecot configuration files

  • YOU must edit and replace {ONION_MAIL} with your onion address, or use the script below

Postfix

main.cnf

# See /usr/share/postfix/main.cf.dist for a commented, more complete version


# Debian specific:  Specifying a file name will cause the first
# line of that file to be used as the name.  The Debian default
# is /etc/mailname.
#myorigin = /etc/mailname

# Mail Dir
home_mailbox = Maildir/

#smtpd_banner = $myhostname ESMTP $mail_name (Debian/GNU)
smtpd_banner = $myhostname ESMTP
biff = no

# appending .domain is the MUA's job.
append_dot_mydomain = no

# Uncomment the next line to generate "delayed mail" warnings
#delay_warning_time = 4h

readme_directory = no

# See http://www.postfix.org/COMPATIBILITY_README.html -- default to 3.6 on
# fresh installs.
compatibility_level = 3.6

# TLS parameters
smtpd_tls_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
smtpd_tls_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
smtpd_tls_security_level=may

smtp_tls_CApath=/etc/ssl/certs
smtp_tls_security_level=may
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache

# TLS Security
smtpd_tls_auth_only = yes
smtp_tls_security_level = encrypt
smtp_tls_note_starttls_offer = yes
smtpd_tls_protocols = !SSLv2, !SSLv3
smtpd_tls_ciphers = high
smtpd_tls_mandatory_ciphers = high

# Dovecot
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes
smtpd_sasl_security_options = noanonymous

#smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination
smtpd_helo_restrictions = permit_mynetworks, reject_invalid_helo_hostname
smtpd_helo_required = yes

myhostname = {ONION_MAIL}
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
myorigin = /etc/mailname
#myorigin = $myhostname
mydestination = {ONION_MAIL}, localhost
relayhost = 
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
mailbox_size_limit = 0
recipient_delimiter = +
inet_interfaces = 127.0.0.1
inet_protocols = all
disable_vrfy_command = yes

Dovecot

dovecot.conf

## Dovecot configuration file

# If you're in a hurry, see http://wiki2.dovecot.org/QuickConfiguration

# "doveconf -n" command gives a clean output of the changed settings. Use it
# instead of copy&pasting files when posting to the Dovecot mailing list.

# '#' character and everything after it is treated as comments. Extra spaces
# and tabs are ignored. If you want to use either of these explicitly, put the
# value inside quotes, eg.: key = "# char and trailing whitespace  "

# Most (but not all) settings can be overridden by different protocols and/or
# source/destination IPs by placing the settings inside sections, for example:
# protocol imap { }, local 127.0.0.1 { }, remote 10.0.0.0/8 { }

# Default values are shown for each setting, it's not required to uncomment
# those. These are exceptions to this though: No sections (e.g. namespace {})
# or plugin settings are added by default, they're listed only as examples.
# Paths are also just examples with the real defaults being based on configure
# options. The paths listed here are for configure --prefix=/usr
# --sysconfdir=/etc --localstatedir=/var

# Enable installed protocols
!include_try /usr/share/dovecot/protocols.d/*.protocol

# A comma separated list of IPs or hosts where to listen in for connections. 
# "*" listens in all IPv4 interfaces, "::" listens in all IPv6 interfaces.
# If you want to specify non-default ports or anything more complex,
# edit conf.d/master.conf.
#listen = *, ::
listen = 127.0.0.1

# Base directory where to store runtime data.
#base_dir = /var/run/dovecot/

# Name of this instance. In multi-instance setup doveadm and other commands
# can use -i <instance_name> to select which instance is used (an alternative
# to -c <config_path>). The instance name is also added to Dovecot processes
# in ps output.
#instance_name = dovecot

# Greeting message for clients.
#login_greeting = Dovecot ready.

# Space separated list of trusted network ranges. Connections from these
# IPs are allowed to override their IP addresses and ports (for logging and
# for authentication checks). disable_plaintext_auth is also ignored for
# these networks. Typically you'd specify your IMAP proxy servers here.
#login_trusted_networks =

# Space separated list of login access check sockets (e.g. tcpwrap)
#login_access_sockets = 

# With proxy_maybe=yes if proxy destination matches any of these IPs, don't do
# proxying. This isn't necessary normally, but may be useful if the destination
# IP is e.g. a load balancer's IP.
#auth_proxy_self =

# Show more verbose process titles (in ps). Currently shows user name and
# IP address. Useful for seeing who are actually using the IMAP processes
# (eg. shared mailboxes or if same uid is used for multiple accounts).
#verbose_proctitle = no

# Should all processes be killed when Dovecot master process shuts down.
# Setting this to "no" means that Dovecot can be upgraded without
# forcing existing client connections to close (although that could also be
# a problem if the upgrade is e.g. because of a security fix).
#shutdown_clients = yes

# If non-zero, run mail commands via this many connections to doveadm server,
# instead of running them directly in the same process.
#doveadm_worker_count = 0
# UNIX socket or host:port used for connecting to doveadm server
#doveadm_socket_path = doveadm-server

# Space separated list of environment variables that are preserved on Dovecot
# startup and passed down to all of its child processes. You can also give
# key=value pairs to always set specific settings.
#import_environment = TZ

##
## Dictionary server settings
##

# Dictionary can be used to store key=value lists. This is used by several
# plugins. The dictionary can be accessed either directly or though a
# dictionary server. The following dict block maps dictionary names to URIs
# when the server is used. These can then be referenced using URIs in format
# "proxy::<name>".

dict {
  #quota = mysql:/etc/dovecot/dovecot-dict-sql.conf.ext
}

# Most of the actual configuration gets included below. The filenames are
# first sorted by their ASCII value and parsed in that order. The 00-prefixes
# in filenames are intended to make it easier to understand the ordering.
!include conf.d/*.conf

# A config file can also tried to be included without giving an error if
# it's not found:
!include_try local.conf


Bonus Script:

This script can act as a backup or installer, using the configurations
from above, below you can find tormail_stack.sh.

#!/bin/bash

set -e

# CONFIG
CONFIG_DIR="$(dirname "$0")/configs"
BACKUP_DIR="/root/tormail_backup_$(date +%F_%H-%M-%S)"
ONION_FILE="/var/lib/tor/mail_hidden/hostname"
PATCH_PLACEHOLDER="{ONION_MAIL}"
HOSTNAME_FILE="/root/tormail_hostnames.txt"

install() {
    echo "[+] Installing TorMail stack..."

    echo "[+] Adding Tor Project onion repository key and source..."
    
    echo "[+] Installing Tor..."
    apt update && apt install -y tor apt-transport-tor
    
    # Add Tor Project GPG key to keyring
    wget -qO- https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc | \
      gpg --dearmor | tee /usr/share/keyrings/deb.torproject.org-keyring.gpg >/dev/null

    # Add Tor Project onion repo to sources list
    cat << EOF > /etc/apt/sources.list.d/tor.list
deb [signed-by=/usr/share/keyrings/deb.torproject.org-keyring.gpg] tor+http://apow7mjfryruh65chtdydfmqfpj5btws7nbocgtaovhvezgccyjazpqd.onion/torproject.org bookworm main
deb [signed-by=/usr/share/keyrings/deb.torproject.org-keyring.gpg] tor+http://apow7mjfryruh65chtdydfmqfpj5btws7nbocgtaovhvezgccyjazpqd.onion/torproject.org tor-nightly-main-bookworm main
EOF

    # Add Debian onion repo to sources list
    cat << EOF > /etc/apt/sources.list.d/debian.list
deb     tor+http://2s4yqjx5ul6okpp3f2gaunr2syex5jgbfpfvhxxbbjwnrsvbk5v3qbid.onion/debian            bookworm         main
deb-src tor+http://2s4yqjx5ul6okpp3f2gaunr2syex5jgbfpfvhxxbbjwnrsvbk5v3qbid.onion/debian            bookworm         main

deb     tor+http://2s4yqjx5ul6okpp3f2gaunr2syex5jgbfpfvhxxbbjwnrsvbk5v3qbid.onion/debian            bookworm-updates main
deb-src tor+http://2s4yqjx5ul6okpp3f2gaunr2syex5jgbfpfvhxxbbjwnrsvbk5v3qbid.onion/debian            bookworm-updates main

deb     tor+http://5ajw6aqf3ep7sijnscdzw77t7xq4xjpsy335yb2wiwgouo7yfxtjlmid.onion/debian-security     bookworm-security main
deb-src tor+http://5ajw6aqf3ep7sijnscdzw77t7xq4xjpsy335yb2wiwgouo7yfxtjlmid.onion/debian-security     bookworm-security main

# Optional backports (may not be populated)
# deb     tor+http://2s4yqjx5ul6okpp3f2gaunr2syex5jgbfpfvhxxbbjwnrsvbk5v3qbid.onion/debian          bookworm-backports main
# deb-src tor+http://2s4yqjx5ul6okpp3f2gaunr2syex5jgbfpfvhxxbbjwnrsvbk5v3qbid.onion/debian          bookworm-backports main    
EOF
    echo "[+] Installing dependencies..."
    apt update && apt install -y tor deb.torproject.org-keyring

    echo "[+] TorMail stack dependencies installed."
   
    echo "[+] Backing up existing configs to $BACKUP_DIR"
    mkdir -p "$BACKUP_DIR"
    cp -r /etc/tor "$BACKUP_DIR/tor"
    
    echo "[+] Replacing tor configs..."
    cp "$CONFIG_DIR/torrc" /etc/tor/torrc
    
    echo "[+] Restarting tor be sure to follow the instructions from README.md for configuration for Postfix during install"
    systemctl restart tor
    
    sleep 4
    
    if [[ ! -f "$ONION_FILE" ]]; then
        echo "[!] Onion hostname file not found. Tor HiddenService may not be ready."
        exit 1
    fi

    ONION_HOST=$(cat "$ONION_FILE")
    echo "[+] Onion Mail hostname [COPY]: $ONION_HOST"

    
    sleep 8
    
    echo "[+] Installing dependencies..."
    apt update && apt install -y dovecot-core dovecot-imapd dovecot-pop3d postfix mailutils
    
    echo "[+] Backing up existing configs to $BACKUP_DIR"
    cp -r /etc/dovecot "$BACKUP_DIR/dovecot"
    cp -r /etc/postfix "$BACKUP_DIR/postfix"

    echo "[+] Replacing configs..."
    cp -r "$CONFIG_DIR/dovecot/"* /etc/dovecot/
    cp -r "$CONFIG_DIR/dovecot/conf.d/"* /etc/dovecot/conf.d/
    cp -r "$CONFIG_DIR/postfix/"* /etc/postfix/

    echo "[*] Updating Resolv.conf {backup} /etc/resolv.conf.bak"

    cp -v /etc/resolv.conf /etc/resolv.conf.bak

    # Filter out 'search' and 'nameserver' lines, then append 'nameserver 127.0.0.1'
    grep -vE '^(search|nameserver)' /etc/resolv.conf.bak > /etc/resolv.conf

    echo "nameserver 127.0.0.1" >> /etc/resolv.conf
    
    echo "[+] Restarting Tor..."
    systemctl restart tor
    #systemctl restart dovecot
    #systemctl restart postfix
    

    sleep 3

    if [[ ! -f "$ONION_FILE" ]]; then
        echo "[!] Onion hostname file not found. Tor HiddenService may not be ready."
        exit 1
    fi

    ONION_HOST=$(cat "$ONION_FILE")
    echo "[+] Onion Mail hostname: $ONION_HOST"

    echo "[+] Replacing onion placeholders in config files..."
    grep -rl "$PATCH_PLACEHOLDER" /etc/dovecot /etc/postfix | while read -r file; do
        sed -i "s/$PATCH_PLACEHOLDER/$ONION_HOST/g" "$file"
    done

    echo "Mail Onion Hostname: $ONION_HOST" > "$HOSTNAME_FILE"
    echo "SSH Onion Hostname: $(cat /var/lib/tor/ssh_hidden/hostname 2>/dev/null || echo 'Not Set')" >> "$HOSTNAME_FILE"

    echo "[✓] TorMail installed. Hostnames saved to $HOSTNAME_FILE"
    
    echo "[Restarting and enabling Dovecot/Postfix/Tor at boot-time]"
    systemctl restart dovecot
    systemctl restart postfix
    systemctl restart tor
    
    # Enable at boot
    systemctl enable tor
    systemctl enable postfix
    systemctl enable dovecot
    
    echo "[Services Available and running...]"
}

backup() {
    echo "[+] Backing up current configs to $BACKUP_DIR"
    mkdir -p "$BACKUP_DIR"
    cp -r /etc/tor "$BACKUP_DIR/tor"
    cp -r /etc/dovecot "$BACKUP_DIR/dovecot"
    cp -r /etc/postfix "$BACKUP_DIR/postfix"
    echo "[✓] Backup complete: $BACKUP_DIR"
}

iptables() {
    echo "[*] Creating /opt/iptables.sh Route all traffic through tor"
    cat << EOF > /opt/iptables.sh
#!/bin/bash

# Flush & drop all
iptables -F
iptables -P INPUT DROP
iptables -P OUTPUT DROP

# Allow already-established sessions
iptables -A INPUT  -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# Allow localhost
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT

# Tor daemon access
iptables -A OUTPUT -m owner --uid-owner debian-tor -j ACCEPT

# Tor DNS and SOCKS only for debian-tor
iptables -A OUTPUT -p tcp --dport 9040 -m owner --uid-owner debian-tor -j ACCEPT
iptables -A OUTPUT -p tcp --dport 9050 -m owner --uid-owner debian-tor -j ACCEPT
iptables -A OUTPUT -p udp --dport 53   -m owner --uid-owner debian-tor -j ACCEPT
iptables -A OUTPUT -p tcp --dport 53   -m owner --uid-owner debian-tor -j ACCEPT

# ALLOW local-only onion forward ports
iptables -A INPUT -p tcp -s 127.0.0.1 --dport 22  -j ACCEPT  # SSH
iptables -A INPUT -p tcp -s 127.0.0.1 --dport 25  -j ACCEPT  # SMTP
iptables -A INPUT -p tcp -s 127.0.0.1 --dport 587 -j ACCEPT  # SMTP Auth
iptables -A INPUT -p tcp -s 127.0.0.1 --dport 143 -j ACCEPT  # IMAP
iptables -A INPUT -p tcp -s 127.0.0.1 --dport 993 -j ACCEPT  # IMAPS
iptables -A INPUT -p tcp -s 127.0.0.1 --dport 110 -j ACCEPT  # POP3
iptables -A INPUT -p tcp -s 127.0.0.1 --dport 995 -j ACCEPT  # POP3S
EOF
    chmod +x /opt/iptables.sh
    echo "[*] Applying non-persistent iptables"
    /opt/iptables.sh    
}

apply_new_hostname() {
    echo "[*] Applying new hostname to configuration files..."

    # Get hostname from Tor Hidden Service directory
    ONION_MAIL=$(cat /var/lib/tor/mail_hidden/hostname 2>/dev/null)

    if [[ -z "$ONION_MAIL" ]]; then
        echo "[!] ERROR: Hostname file not found or empty at /var/lib/tor/mail_hidden/hostname"
        return 1
    fi

    echo "[*] Hostname detected: $ONION_MAIL"
    echo "[*] Replacing {ONION_MAIL_REPLACE} in config files..."

    # Replace placeholder in postfix configs
    echo "[*] Updating Postfix configuration files..."
    grep -Rl '{ONION_MAIL_REPLACE}' /etc/postfix | while read -r file; do
        echo "  [+] $file"
        sed -i "s/{ONION_MAIL_REPLACE}/$ONION_MAIL/g" "$file"
    done

    # Replace placeholder in dovecot configs
    echo "[*] Updating Dovecot configuration files..."
    grep -Rl '{ONION_MAIL_REPLACE}' /etc/dovecot | while read -r file; do
        echo "  [+] $file"
        sed -i "s/{ONION_MAIL_REPLACE}/$ONION_MAIL/g" "$file"
    done

    echo "[✓] Hostname applied to Postfix and Dovecot configs."
    
    echo "[Restarting and enabling Dovecot/Postfix/Tor at boot-time]"
    systemctl restart dovecot
    systemctl restart postfix
    systemctl restart tor
    
    systemctl enable tor
    systemctl enable postfix
    systemctl enable dovecot
}

help () {
    echo "[*] TorMail Stack"
    echo "[*]"
    echo "[*] Installs Postfix/Dovecot/Tor"
    echo "[*]"
    echo "[*] How to use:"
    echo "[*]"
    echo "[*] ./tormail_stack.sh install   | INFO | {installs all dependencies & configuration files}"
    echo "[*] ./tormail_stack.sh backup    | INFO | {backup Postfix/Dovecot/Tor configuration files}"
    echo "[*] ./tormail_stack.sh iptables  | INFO | {route all traffic through tor}"
    echo "[*]"
    echo "[*] DEBUG "
    echo "[*] ./tormail_stack.sh appy_new_hostname {replace ALL config files from configs to appropriate directory & run this}"
    echo "[*]"
}
usage() {
    echo "Usage: $0 {install|backup|iptables|apply_new_hostname}"
    exit 1
}

case "$1" in
    install) install ;;
    backup) backup ;;
    iptables) iptables ;;
    appy_new_hostname) appy_new_hostname ;;
    *) usage ;;
esac

Usage:

  • ./tormail_stack.sh install
  • ./tormail_stack.sh backup
  • ./tormail_stack.sh iptables