Email Postfix in Practice

Categories: Linux

(Back to the main article)


This article describes the steps needed to install Postfix + Dovecot + SpamAssassin on an Ubuntu server. The implemented solution is very light-weight, and suitable for handling mail for a small number of users (eg a single person, a family or small startup). In particular, user-accounts are defined via config-files (not a database), in order to use the least amount of resources.

The reasons for choosing such a setup are described here. A general overview of email is described here. Some background information on Postfix can be found here.

The instructions describe how to set up email for on a server with name Replace with your domain-name.

The solution described here uses the Postfix virtual delivery transport, with email-accounts defined in a hand-editable plain text file.

Setting up a Test Email Domain

The steps below are rather complicated, and getting it right first time is not easy. If you are migrating an existing email-domain from a hosted service to a self-configured system then it can be awkward to switch over and find the new system isn’t working right.

If you are migrating an existing email domain, then I recommend first setting up a new email system for a “test domain”, and switching the real domain over after confirming that emails via the test-domain work. Assuming you have a domain-name, it is possible to apply the following instructions to set up email-domain, send and receive emails, then update to handle the real addresses - mostly just a case of replacing with below…

Create an SSL Certificate

It is assumed that you have already obtained an SSL certificate for “” (replace with your domain).

Configure the System Firewall

Ubuntu automatically uses UFW to block most incoming ports by default. Run ufw status verbose to see the current rules; if ufw is active then:

  • ufw allow 465 # smtp-over-tls
  • ufw allow 587 # submission
  • ufw allow 993 # imap-over-tls
  • ufw allow 220 # imap (hopefully with STARTTLS)

Install Dovecot

Before setting up Postfix to handle incoming email, we should set up the system that actually stores those emails on disk, and provides access to them. Dovecot does not send emails (that is done by Postfix) nor does it accept incoming emails from external email-servers; it accepts emails from the local Postfix (via lmtp), and handles email-client-applications connecting via the IMAP protocol to view/manage/delete existing emails. Outgoing emails from an email-client-application point at the Postfix “submission port”, not at dovecot.

Install dovecot with:

  • apt-get install dovecot-core dovecot-imapd dovecot-lmtpd

In /etc/dovecot/conf.d, configure files as follows. Note that commented-out lines in the installed config-files indicate the default values.

Dovecot Auth

Dovecot has an extremely flexible/configurable system for configuring email accounts/users. This is used to check passwords for IMAP clients (and outgoing potsfix email via sasl), and to specify where/how user emails are to be stored.

For a setup with a very small number of users and a dedicated admin, storing user-info in a plain file is easiest and uses the least resources on the system. The default “auth-system” settings instead use the PAM login framework for user passwords, but (a) that makes obtaining user-related info needed in other contexts difficult and (b) it seems a bad idea to link email passwords to login passwords. Having a simple file for user information and credentials is easier to understand and debug, and seems safer.

Edit 10-auth.conf to:

  • include only auth-passwdfile.conf.ext (see end of file)
  • set auth_username_format = %Ln (alternative is to use full user@domain names in /etc/dovecot/users and in the email-client “userid” field).

Now edit auth-passwdfile.conf.ext:

passdb {
  driver = passwd-file
  args = username_format=%u /etc/dovecot/users

userdb {
  driver = passwd-file
  args = username_format=%u /etc/dovecot/users

A dovecot “passdb” indicates where (username -> password) mappings can be found to authenticate users over imap or sasl. A dovecot “userdb” instead specifies where (username -> userinfo) mappings can be found; for some email-storage-mechanisms dovecot needs to know a user’s “home dir” and UID in order to write files. This article actually sets up email storage in such a way that this info is irrelevant, but the userdb is still required. The passdb and userdb can be separate files, or can be a combined file in /etc/passwd-style layout.

The specified file /etc/dovecot/users should look somewhat like this:

# SHA512 passwords created via "sha512sum<enter>password<ctrl-d><ctrl-d>" then copy-and-paste

Ensure the file has suitable access-rights! It must be readable by the user specified in “service auth” and in “service auth-worker” (10-master.conf) who is by default root. A safer config is to set these entries to user=$default_internal_user and ensure the file is readable by that user (dovecot).

As mentioned, this is basically /etc/passwd format with “columns” holding username, passwd, userid, groupid, description, shell, home-dir.

However the “passwd” column holds a password-hash prefixed with the hash-method used, so that dovecot can hash an incoming password and compare it to the expected value.

When dovecot is configured to deliver emails to a user’s homedir then the homedir is of course relevant; however in this article dovecot is instead configured to store email under /var/dovemail/{user} so the homedir in this file is not actually relevant. Neither is description or shell, so only 4 columns are needed.

If you have problems with authentication, try editing /etc/dovecot/conf.d/10-logging.conf and setting the “verbose” options to “yes” to get more info in /var/log/mail.log.

By default, unencrypted-logins are disabled, which is what we want.

Dovecot Delivery

Edit file conf.d/10-mail.conf to specify where email is to be stored in the local filesystem. Set

mail_home = /var/dovemail/%u
mail_location = maildir:/var/dovemail/%u/Maildir

This will store user-specific settings for user me under /var/dovemail/me, and emails under /var/dovemail/me/Maildir. The “home” column of the userdb (file /etc/dovecot/users) will be ignored (and can be empty).

The files will be owned by the UID specified in the Dovecot userdb. When accessing emails on behalf of a user, dovecot will “switch user” to the UID specified in this file, in order to have the needed filesytsem read/write access. An alternative is to define a new system user to “own all emails” (by convention, name=vmail) and specify this via config-setting mail_uid. The file defining users can then omit userids completely.

Dovecot Services

Edit to define which ports/sockets Dovecot will listen on. Postfix will be set up to not deliver (write) emails itself but instead to pass them on to Dovecot for storage; the protocol between Postfix and Dovecot will be LMTP over a local filesystem socket, so configure Dovecot to create/listen on that socket:

service lmtp {
  unix_listener /var/spool/postfix/private/dovecot-lmtp {
    group = postfix
    user = postfix
    mode = 0600

Postfix will also be configured to use Dovecot’s password-database to authenticate users who perform SMTP-AUTH with Postfix (ie local users wanting to send email over the Postfix submission-port). Postfix will communicate with Dovecot using the SASL protocol over a local filesystem socket, so configure Dovecot to listen on that:

service auth {
  unix_listener /var/spool/postfix/private/auth {
    mode = 0666

See this dovecot wiki page for more information on Postfix+Dovecot+LMTP.


Connections from email clients to Dovecot’s IMAP port should always be encrypted. That means Dovecot needs to be given access to the SSL server-certificate for

I presume you’ve already created such a certificate, so now edit conf.d/10-ssl.conf:

ssl = required
ssl_cert = </etc/letsencrypt/live/
ssl_key = </etc/letsencrypt/live/


Edit conf.d/15-lda.conf and set the postmaster_address to

Starting Dovecot

Just run “systemctl dovecot.service restart” (or service restart dovecot) to pick up all the changes. Check /var/log/mail.err and /var/log/mail.log for any error-messages.

Testing Dovecot

From the dovecot wiki:

  openssl s_client -connect
  A1 LOGIN username password
  A2 LIST "" "*"

Other options:

  • doveadm user someuser checks whether user-lookup of the specified user works
  • doveconf userdb displays the current userdb configuration
  • use sendmail (after postfix is configured) - which will place email directly on mailqueue bypassing smtpd.

See /var/log/mail for messages.

Install Postfix

Postfix can be simply installed via:

  • apt-get install postfix

Set up Submission Port

Edit /etc/postfix/ to add an extra SMTPD server specifically for outgoing email:

submission inet n       -       y       -       2       smtpd -v
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_sasl_type=dovecot
  -o smtpd_sasl_path=private/auth
  -o smtpd_reject_unlisted_recipient=no
  -o smtpd_recipient_restrictions=$submit_recipient_restrictions
  -o smtpd_relay_restrictions=$submit_relay_restrictions
  -o milter_macro_daemon_name=ORIGINATING

Don’t be worried about the many other “services” defined in this file; they don’t “run” unless needed - and our configuration will never need them.

The meaning of the entries here is described in the postfix email theory article.

Local delivery vs Virtual delivery

As described in the “postfix theory” article, postfix smtpd first categorizes incoming email into:

  • “local” (when the recipient address domain is in $mydestination)
  • “virtual” (when the recipient address domain is in $virtual)

For emails with class=local, it then checks $local_recipient_maps to see if the recipient user exists - which looks in /etc/passwd and /etc/aliases by default. Assuming that passes, the emails are then delivered using the service specified in $local_transport; that is mapped in to an actual application to execute - usually /usr/lib/postfix/sbin/local. That then looks up the user in /etc/passwd again then usually writes emails directly to a file, although $mailbox_transport can be used to pass the email elsewhere, eg to Dovecot.

A similar process is applied to incoming emails with class=virtual, except that custom files are used instead of /usr/passwd, and that the “email domain” remains attached to the “userid”, ie virtual-delivery treats the recipient as “”, not as “user1”.

For a simple email-server handling just one email-domain and with just a few users, either approach works. Using the local approach does require each email-recipient to have a native unix account on the mailserver, but it does not need to have login rights and for just a few users that is no great problem. And it allows per-user “forwarding files”, ie each user can determine if/where their email gets forwarded to. I initially set up my postfix configuration with my target email-domain in $mydestination (thus categorising incoming emails as class=local), together with configuring $mailbox_transport to use LMTP to forward emails to Dovecot. However there is one disadvantage: setting up a catch-all email account (ie one to which email for unknown-users is saved) is very difficult (maybe impossible); the local delivery agent supports $luser_relay for this use-case, but that does not work in combination with LMTP, and I could not figure out how to configure catch-all behaviour on the Dovecot side.

I therefore use virtual delivery in this solution, even though I support only one email domain. Postfix users are defined in a plain text file (/etc/postfix/virtual) and Dovecot users are defined in a separate plain text file (/etc/dovecot/users). Having duplicated configuration is a shame, but the required file formats are different. Given a small number of users, this duplication is acceptable. Local delivery also requires duplicated entries (as far as I can tell). Having just a single definition for users between postfix and dovecot appears to only be possible when using an SQL database (both postfix and dovecot can then be configured to use the same SQL tables). Fortunately, setting up a “catch-all” account for postfix virtual addresses is trivial, as is setting up email-forwarding (although changing the forwarding-address must be done by the mail-admin, not an end-user).

Configure Postfix

Postfix is a set of about a dozen separate applications. Each application has a set of variables that influence how it behaves; these variables have builtin defaults (usually very sensible ones) which can be overridden in file and those can be overridden via “-o” options on the commandline (as shown above).

So now edit /etc/postfix/ as follows:

# Postfix Config settings for services started in
# See /usr/share/postfix/ for a commented, more complete version
# Note that these are _defaults_ for services, which can be overridden on the commandline specified in

# Indicate that this config-file is version-2-format, and that backwards-compat settings
# for earlier config-formats are not wanted

# Basic settings
# Note that when $mydestination includes "" then incoming mail for will be considered "local".
myorigin = /etc/mailname
mydestination = $myhostname, localhost, localhost.localdomain
mynetworks = [::ffff:]/104 [::1]/128
mailbox_size_limit = 0
recipient_delimiter = +
inet_interfaces = all
append_dot_mydomain = no

# Send outgoing email direct to destination, not via some intermediate
relayhost =

# Do not allow smtpd to accept emails for anyone outside of the local domain.
# Note that incoming email is accepted only if it passes BOTH of smtpd_relay_restrictions AND smtpd_recipient_restrictions
relay_domains =
smtpd_relay_restrictions = reject_unauth_destination

# Rate Limiting
smtpd_recipient_limit = 5
smtpd_client_recipient_rate_limit = 50

# TLS parameters
# See /usr/share/doc/postfix/TLS_README.gz in the postfix-doc package for
# information on enabling SSL in the smtp client.
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
smtp_tls_protocols=!SSLv2, !SSLv3

# Handing incoming LOCAL email
# Aliases are transformed via alias_maps. Then the user is expected to exist in /etc/passwd (else email rejected).
# Then the email is accepted (sender is given an accepted-code) and the "local" delivery agent is applied. This
# delivery agent looks for ".forward" files, then uses local_transport
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases

# Handling incoming VIRTUAL mail
virtual_alias_domains =
virtual_alias_maps = hash:/etc/postfix/virtual

# Use service lmtp (from for all incoming mail that matches $virtual_alias_domains, and pass that
# service some extra parameters ("unix:private/dovecot-lmtp") that indicate which socket to pass data over.
virtual_transport = lmtp:unix:private/dovecot-lmtp

#defer_transports = smtp
#smtpd_log_access_permit_actions = static:all

smtpd_banner = $myhostname ESMTP $mail_name (Ubuntu)
biff = no
readme_directory = no

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

# Custom variables used by the "submission" smtpd instance (see
submit_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination, reject
submit_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, defer_unauth_destination, reject

# Custom variables used by the "primary" smtpd instance (see
smtpd_client_restrictions =

smtpd_recipient_restrictions =
  reject_unauth_pipelining, reject_non_fqdn_recipient, reject_unknown_recipient_domain, reject_unauth_destination

There is a lot to explain here…

Most of these settings are for use by the SMTPD daemon, although some are also used by the postfix-variant of sendmail which is used by Spamassassin among other things.

Variable mydestination has been set up so email to “user1@localhost” is still delivered as a “local” email (ie assumes there is a native unix account named user1). However this variable does not include “” so such emails will not be classified as local.

The relay options are set to values that effectively disable “explicit relaying”. Here, the word relay means relaying between cooperating email servers, eg within a single company. We are setting up a personal email-server here, so this kind of “relay” is not needed. Later, the word relay is used to mean sending email to arbitrary email-servers on the internet; this reuse of terminology is unfortunate and confusing.

The SSL entries are hopefully obvious. Inbound email coming from external email-servers should really be transferred over an encrypted link for privacy, New emails being submitted by local users (via email client apps) over SMTP should definitely be encrypted to hide the transfer of passwords etc. And this encryption requires Postfix to have access to an SSL server certificate for Outbound email from from this server to external email-servers should of course also be encrypted, but in that case the certificate is the responsibility of the remote server. Note that the “submission” SMTP server may require SSL (and we do set this up via a “-o” option in, but the SMTPD service on port 25 is not supposed to require SSL, just to offer it as an option.

The standard aliases are applied to emails of class “local”.

The email domain “” (ie emails like is defined as a recognised virtual domain. The valid users for this domain are defined in file /etc/postfix/virtual. A “virtual_alias_maps” file defines not only which users exist, but also optionally defines “forwarding rules” equivalent to the “dot-forward” feature of the local-delivery-agent, eg forwarding all emails for a user to And the file can also define a “catch-all” target email address. As noted earlier, there is duplication between this file and /etc/dovecot/users that is unfortunately unavoidable AFAICT; there is no common file-format supported by both dovecot and postfix. Unified data is possible via an SQL database, but that is not worth-while for a small email server.

The virtual_transport setting indicates that emails for should be passed via lmtp to Dovecot.

The commented-out “defer_transports=smtp” can be useful when trying to get Postfix set up; it ensures that all email queued for the smtp-service to send out over the internet will instead be placed “on hold” until explicitly sent via postqueue -f to “flush queues”. It is then possible to check the deferred queue first before flushing the queues, to ensure no spammer has managed to route emails via your postfix instance due to incorrect postfix rules. Once your email server is registered in DNS (see later), spammers will start probing it within minutes. If your server is temporarily misconfigured, and starts forwarding such spam mails, then you can quickly land on a black-list from which it is difficult to get removed again. So blocking outgoing email can be a nice initial safety-net.

Now we come to potentially the most confusing part: client-restrictions, recipient-restrictions and relay-restrictions.

Client-restrictions are used to reject entire remote mailservers, ie prevent a mailserver from submitting any mail. Here we reject any remote email-server which is on one of the two major blacklists.

Recipient-restrictions are used to reject specific emails on the basis of the recipient address (“to-address”). For the smtpd instance that handles incoming email, we expect only email to; the reject_non_fqdn_recipient ensures the address does specify a domain, and reject_unknown_recipient_domain ensures the domain is The other two reject specific operations except for destinations which are explicitly listed in the relay_hosts setting; as this isn’t an email-server for a big company the relay-hosts was set to empty and so these operations are always rejected (note: auth here means authorized ie “specified in config-file”, not authenticated). For the “submission” smtpd instance which handles outgoing email from my desktop email client, specified submit_recipient_restrictions and the restrictions defined in that variable instead reject all emails unless the submitting application has successfully performed a “login via sasl” (see sasl later).

Relay-restrictions are also used to reject specific emails on the basis of the recipient address; an email will only be accepted by spamd if it passes the tests in both lists (see postfix theory article for why two lists are a good idea).

Phew - done. Now emails are rejected on port 25 unless destined for a user defined in the virtual_alias_maps, and rejected on port 587 unless coming from a logged-in user.

Note on SASL Authentication

An email-server should be very careful about sending email into the internet unless it knows that the email is from a “trustworthy source”. Applications running on the local host are generally considerd “sufficiently trustworthy”, as are applications which authenticate themselves via (username, password). Postfix thus supports authentication - but Dovecot also needs to authenticate users who want to view email via IMAP, and it is not good to have separate (user,pwd) databases for each. There are several possible solutions, but the most common is to make Dovecot the master “authenticator” and for Dovecot to offer authentication as a service on a local filesystem socket. Postfix can then be configured to pass on (user, hash) pairs to this socket, and read the ok/fail response back. Actually, the SASL protocol is more complicated than that, but the details are not relevant here. Note that checking which users exist is needed for incoming email, and this is not checked over SASL - only logins performed by users wanting to submit outgoing email. This can lead to some duplicated config - but for a few users the nuisance is bearable.

Restart Postfix

Run service postfix restart to pick up all changes. Check /var/log/mail.err and /var/log/mail.log for messages. If you have problems, any application listed in can have “-v” added to its options to increase the amount of logging it does (as shown above for the submission sevice).

Define email accounts

Define users in /etc/dovecot/users and in /etc/postfix/virtual as appropriate.

Note that a “local aliases” file has lines of form “aliasname:realname” while “virtual aliases” files have lines of form “aliasaddress <whitespace> realaddress”.

After updating /etc/postfix/virtual, run postmap /etc/postfix/virtual to generate file /etc/postfix/virtual.db.


You should now be able to use a desktop email client to login-in to the submission port, set up encryption, and submit an email from to After a few seconds, the email should appear in the IMAP inbox for the same account. Aliases should also work.

However before sending email to other hosts, DNS should be set up properly.

Useful postfix commands

Command postsuper can perform a range of useful functions, eg deleting all mail from a queue. Can only be executed by root. Examples:

  • postsuper -d ALL – delete all queued emails

Command postqueue can perform some other useful functions, and can be run by a regular user.

Command postconf shows the current configuration (and with the right flag, the default configuration). Examples:

  • postconf -d – displays default settings for all variables
  • postconf -n – displays all variables which differ from the default value (ie which have been overridden locally)

Command postfix can also perform some useful functions, eg:

  • postfix flush – send all queued mail now

Configure DNS

Now we need to inform other systems that email should be directed to this server, and set up some safety-nets to prevent other servers from impersonating our domain when sending spam.

  • ensure an A-record maps>ip4-address
  • ensure an AAAA-record maps>ip6-address
  • ensure a PTR record exists for (ie maps ip4-address back to
  • define TXT records for SPF
  • define CNAME record for ->
  • define MX record for ->

The first step is to ensure that the domain-name-registrar through which the base domain-name ( is registered has registered NS records for the domain which point at a set of DNS servers that can be further configured. Some registrars also provide DNS servers and a suitable web-page. Virtual-hosting companies also do. In my case, the mail-server runs on a virtual server rented from, and provide DNS servers for their hosted systems; I therefore use my domain-name-registrar admin page to register NS records that point at the digitalocean domain-name servers and then register the above PTR/TXT/CNAME/MX records via an admin-page for the digitalocean DNS.

The A-record is the “standard” record for DNS; when somebody types into a browser address-bar, or runs ping then the A-record or AAAA-record is retrieved for that name to find the “real” address.

The PTR record is used by email-servers to filter out “real servers” from hacked desktops, routers, or other systems which have been taken over and used to generate spam; such things have an IP-address but won’t have a PTR record. The incoming ip-address is always available to the receiving email-server, so it can use that address as the key for a DNS-lookup of the PTR record. If no record exists, that is a strong indication that the remote system is not a “serious” server. The resulting server-name can also be used when verifying SPF records.

Each email requires a “sender address”. Spammers have a nasty habit of using the addresses of real (innocent) people as the sender-address in their spam output. Many email servers therefore implement the Sender Policy Framework (SPF); for each email they take the supposed sender-email-domain (eg “” has email-domain “”) and perform a lookup of TXT records that have that key. If a TXT record is found which looks like an SPF-record then the data in that record will hold the host-name of all servers (usually one) that are permitted to generate emails with that address. This is then compared with the hostname found via the PTR-record lookup; a mismatch means the remote system is using faked email addresses and they are rejected. In other words, once you have registered an SPF record for> then no spammer can generate spam with fake sender-addresses that claim to be from your email-domain. And that can help keep your system off blackhole-lists. The exact syntax for SPF records is non-trivial, but the following is an initial guide:

v=spf1 +mx +a ip4:111.222.333.444 -all

The CNAME record is useful here because I want to pay for just one server which not only runs email but also hosts a website and various other stuff. However SSL certificates are issued for a specific hostname (aliases are possible, but tricky and letsencrypt doesn’t support them). A CNAME record is an elegant solution - it gives a dedicated hostname (eg but points to the shared server. Registering an additional A-record is not so useful because digitalocean will only register a PTR record for the “real” hostname of a virtual server. The major email servers are fortunately smart enough to realize that>>> is a valid setup, and don’t require the PTR record to map back to

And the MX record says that email destined for “” should be sent over a socket opened to - ie maps between two different “namespaces” (mail-namespace to host-namespace).

There is yet another framework designed to frustrate spammers - DKIM. Hopefully I can cover that in another article in the future. However it doesn’t seem critical to implement that straight away; I’ve had no problems without it so far.

Configure the Email Client

The client should read email using IMAP on port 993.

The client should send email using SMTP on port 587 (the submission port). Trying to send emails via port 25 will just trigger “relaying not permitted” which is exactly correct - forwarding of emails received on port 25 would be very bad for us, very good for the spammers.

In both cases, the username to login with is without domain (assuming you configured auth_username_format = %Ln in dovecot, as recommended), and the password is whatever was entered in /etc/dovecot/users.


Spamassassin can be installed with:

  • apt-get install spamassassin spamc
  • systemctl enable spamassassin.service

The remaining configuration needed for spamassassin is described in the postfix theory article.

Spamassassin simply assigns a “spam score” to each email; if you wish to take some action based upon that score then one option is to use sieve.

Spamassassin scores email based upon a set of rules which are initially installed into /usr/share/spamassassin. When installing spamassassin on Ubuntu via apt-get then /etc/cron.daily/spamassassin is also installed to ensure these rules are kept up-to-date via the sa-update tool. Somewhat confusingly, the updated rules are not written into /usr/share/spamassassin but into /var/lib/spamassassin/3.004001 instead. They are then “compiled” into /var/lib/spamassassin/compiled; presumably this is the same location that the original rules in /usr/share/spamassassin are compiled into, and thus spamd (the spamassassin daemon) sees the updated rules.

The spamassassin wiki has further information on the sa-update tool. Unfortunately, at the current time the instructions on the wiki refer to which no longer exists; the spamassassin project recently moved to the Apache foundation, and it appears that some things have changed but not yet been updated in the wiki. By the way, there appears to be a bug in the /etc/default/spamassassin file used by /etc/cron.daily/spamassassin: it sets PIDFILE=/var/run/ but the systemd spamassassin file writes the pid to /var/run/ However the cron-script does not use this variable.

Configuring New Users

Just to repeat for clarity: with the setup described above, there are two sources of user information: postfix and dovecot. The process of adding a new user is therefore:

  • edit /etc/dovecot/users to add the username and password
  • edit /etc/postfix/virtual to add the user then execute postmap virtual to generate file virtual.db

Other Notes

I found that as soon as I registered an MX record in DNS, the number of login-attempts via SSH as user root increased. It is therefore probably worth checking that you have login-as-root disabled in ssh (ie login as another user, then su to root): file /etc/ssh/sshd_config should contain PermitRootLogin no.

Setting up fail2ban is also recommended.