메일서버란
이메일 보낼때 쓰는 서버 아닐까요? 상식적으로 생각합시다 우리
관련 프로토콜
SMTP
imap
구축에 필요한 프로그램
Postfix
dnf install postfix postfix-mysql
dovecot
dnf install dovecot dovecot-mysql
rspamd
clamAV
아키텍처
![[Pasted image 20230921213641.png]]
![[Pasted image 20230921213705.png]]
서버 구축 과정
mariadb
테이블 스키마
CREATE DATABASE mail_server;
ALTER DATABASE mail_server CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
이메일 주소는 local과 domain으로 구성되며 각 항목별 규칙은 rfc3696에 정의되었다. rfc3696 공식문서 위키피디아
다음은 서버에서 관리할 도메인을 지정하는 테이블이다.
CREATE TABLE domains (
domain varchar(64) NOT NULL PRIMARY KEY
);
각 도메인에 가입된 user 테이블은 다음과 같다.
- local의 경우 rfc3696과 다르게 40자로 제한하였다. 이는 대부분의 이메일 벤더가 사용자의 id를 local로 사용하기에 id의 제약조건을 local에 적용하였다.
CREATE TABLE users (
local varchar(40) NOT NULL,
domain varchar(64) NOT NULL REFERENCES domains(domain) ON DELETE RESTRICT,
password_hash varchar(256) NOT NULL,
display_name varchar(40) NOT NULL,
PRIMARY KEY(domain, local)
-- entire e-mail address should not exceed 254 characters (RFC 3696)
-- postgres syntax : CHECK(char_length(local || domain) <= 254)
);
aliases 테이블은 이메일의 포워딩을 지정한다. ex) [email protected]
-> [email protected]
의 경우 boss로 온 이메일은 모두 user1234의 메일함으로 포워딩된다.
- source는 alias email -> destination은 포워딩될 real email을 의미한다.
CREATE TABLE aliases (
source_local varchar(40) NOT NULL,
source_domain varchar(64) NOT NULL REFERENCES domains(domain) ON DELETE RESTRICT,
destination_local varchar(40) NOT NULL,
destination_domain varchar(64) NOT NULL,
PRIMARY KEY(source_local, source_domain),
FOREIGN KEY (destination_domain, destination_local) REFERENCES users (domain, local) ON DELETE CASCADE
-- entire e-mail address should not exceed 254 characters (RFC 3696)
-- postgres syntax : CHECK(char_length(source_local || source_domain) <= 254)
-- destination needs no check because it references already existing (and already checked) rows.
);
또한 다음과 같은 view를 생성한다. 이 view는 postfix 및 dovecot이 참조한다.
Question: 왜 굳이 view를 쓰나요?
Answer: 위 스키마는 local과 domain이 분리된 column을 갖는다. 하지만 postfix와 dovecot은local@domain
구조의 문자열을 검색하기에 두 column을 join해서 보여주는 새로운 view를 만든다.
users_fqda table은 user table을 fqda 형식으로 보여준다.
Question: fqda가 뭔데 씹덕아
Answer: Fully qualified domain address ex) [email protected]
CREATE VIEW users_fqda AS
SELECT CONCAT(users.local, '@', domains.domain) AS "fqda", users.password_hash, users.display_name
FROM users, domains
WHERE users.domain = domains.domain;
aliases_fqda table
CREATE VIEW aliases_fqda AS
SELECT CONCAT(source_local, '@', source_domain) AS "fqda"
FROM aliases;
db 사용자 구성
mail_user 계정은 postfix와 dovecot이 사용한다. 이들은 db를 조회만 하기에 CUD에 대한 권한은 부여하지 않는다.
CREATE USER 'mail_user'@'localhost' identified by 'YOUR_PASSWORD';
GRANT SELECT ON * TO mail_user@localhost;
Mockup data for test
insert into domains values ('nahida.net');
insert into users(local, domain, password_hash, display_name)
values ('1000dreams', 'nahida.net', '$2y$10$T03RmF8PZVdEGvqMBA7JDe7kleiWmA8Acpoz7iUm7N44P1yiS5tnG', '천일밤의꿈'),
('deepwood', 'nahida.net', '$2y$10$T03RmF8PZVdEGvqMBA7JDe7kleiWmA8Acpoz7iUm7N44P1yiS5tnG', '숲의기억');
INSERT INTO aliases (source_local, source_domain, destination_local, destination_domain) VALUES
('alias', 'nahida.net', 'deepwood', 'nahida.net');
- domain table에 호스팅 할 도메인
nahida.net
을 insert하였다.
이 값은 이메일의 domain part에 해당하는 값(\<local>@==\<domain>==) 이다. smtp 서버가 실제로 이메일을 송수신할 도메인이 아니다.
- 위 도메인에서 사용할 user들을 users table에 insert하였다. 보안을 위해 모든 password는 bcrypt로 해싱 되었으며 원문은
test1234
이다. [email protected]
으로 수신된 메일이[email protected]
으로 포워딩될 수 있도록 aliases table에 해당 map을 insert하였다.
Postfix
가상메일 쿼리문
postfix는 Linux 시스템에 로그인 가능한 user를 위한 SMTP 서버다. 즉 이 체계하에서는 모든 이메일 사용자는 smtp서버에 각자의 linux 계정이 있어야 한다(adduser
). 이러한 방식이 아닌 db에 메일 도메인 및 사용자를 저장하기 위해서는 postfix의 virtual-mailbox 기능을 사용해야 한다.
postfix에서 위에서 생성한 db table에 접근하고 질의 할 수 있도록 지시 파일을 생성해야 한다.
- domains : postfix에서 관리할 domain의 조회에 사용
- mailboxes : postfix에서 관리할 mailboxes의 조회에 사용 (상기한 것처럼 mailbox는 각 메일주소와 거의 동일한 의미를 지닌다.)
- aliases : postfix에서 메일을 포워딩 할 때 필요한 source, destination map의 조회에 사용
이하의 모든 작업은
root
로 진행한다.root
접근이 제한되는 경우sudo
명령어로root
권한을 획득해야 한다.
다음의 위치에 디렉토리를 생성하고 이동한다.
mkdir /etc/postfix/virtual.d && cd /etc/postfix/virtual.d
위에서 설명한 세 파일을 생성한 후 권한을 설정한다.
touch domains.cf mailboxes.cf aliases.cf
chmod 644 /etc/postfix/virtual.d/*.cf
생성한 파일에 다음과 같이 작성하자. 쿼리문에서 사용하는 format string들의 의미는 다음과 같다.
- %u : 로컬
- %d : 도메인
- %s : fqda
domains.cf
user = mail_user
password = <DB_PASSWORD>
hosts = 127.0.0.1
dbname = mail_server
query = SELECT domain FROM domains WHERE domain='%s'
postfix에서 관리하는 virtual-domain에 대한 쿼리문이다.
여기서 domain='%s'
가 아니라 %d
로 주면 쿼리가 동작을 안하는데 이유를 몰겠음…
mailboxes.cf
user = mail_user
password = <DB_PASSWORD>
hosts = 127.0.0.1
dbname = mail_server
query = SELECT fqda FROM users_fqda WHERE fqda='%s';
postfix에서 관리하는 가상사서함(하나의 메일 주소는 각자의 사서함을 지니므로)에 대한 쿼리문이다.
aliases.cf
user = mail_user
password = <DB_PASSWORD>
hosts = 127.0.0.1
dbname = mail_server
query = SELECT CONCAT(destination_local, '@', destination_domain) FROM aliases WHERE source_local='%u' AND source_domain='%d';
postfix가 alias된 메일을 포워딩 하도록 source, destination map을 조회하는 쿼리문이다.
위에서 지정한 세 파일을 검증하기 위해 shell에 다음의 command를 입력한다.
postmap -q nahida.net mysql:/etc/postfix/virtual.d/domains.cf
nahida.net
이 출력된다면 올바르게 설정되었다는 뜻이다. -q
옵션에 적은 string을 주어진 DSN file의 format string을 대체한다. 동일한 방식으로 이메일 주소와 alias 설정도 확인할 수 있다.
$ postmap -q [email protected] mysql:/etc/postfix/virtual.d/mailboxes.cf
[email protected]
$ postmap -q [email protected] mysql:/etc/postfix/virtual.d/mailboxes.cf
[email protected]
main.cf
postfix는 main.cf
와 master.cf
파일로 서버를 구성한다. 이때 main.cf
는 모든 서비스에 전역적으로 설정값이 적용되고, master.cf
는 postfix의 각 서비스를 세부적으로 설정할 때 사용된다. 이때 main.cf
와 master.cf
의 설정에 충돌이 발생하면 전역설정이 아닌 개별설정, 즉 master.cf
의 설정이 우선된다.
이하의 key에 대해 value를 지정하자. 만일 해당 key가 없다면 새로 작성하면 된다.
터미널에
postconf key=value
명령어로도 지정이 가능하지만 여기서는 사용하지 않는다.
다음은 smtp 통신을 위해 서버의 이름과 smtp 통신 시 전달할 내용에 대한 설정이다.
myhostname = smtp.nahida.net
mydomain = nahida.net
mail_name = nahidaSMTP
smtpd_banner = $myhostname ESMTP - $mail_name
smtp_helo_name = $mydomain
myhostname
은 smtp 통신간 상대방에게 광고할 hostname이다.mydomain
은 naked domain을 작성한다.mail_name
은 메일서버 이름이다. default value는Postfix
기에 smtp서버로 postfix를 사용함을 외부에 노출한다. 따라서 이 값을 적절히 바꿔주자.smtpd_banner
는 처음 smtp통신을 시작할 때 서버에서 클라이언트로 전송하는 내용이다. 원하는 내용을 작성하면 되지만 RFC 규정에 의해 반드시 시작은$myhostname
이어야만 한다.smtp_helo_name
은 helo 수신시 응답할 값인데 어디로 응답되는지는 나도 못찾았다. 다만 이 값이$mydomain
이 아닐경우 SpamAssassin등의 스팸탐지기에서SPF_HELO_NONE
태그를 달고 스팸 점수를 올리니$mydomain
으로 두면 된다.
Question :
SPF_HELO_NONE
이 뭔데 씹덕아
Answer : HELO시 수신하는 값이 spf txt랑 일치하지 않아서 발생하는 문제로 DNS에서 spf txt를 질의하려면nahida.net
으로 질의하므로 저 값도 질의 이름과 동일하게 지정한다. 만일 메일주소가[email protected]
이 아니라[email protected]
이라면 spf txt 또한mail.nahida.net
으로 질의하고 이 경우에는smtp_helo_name
을mail.nahida.net
으로 지정해야 한다.
spf 질의법은 다음과 같다.
- 리눅스
dig txt nahida.net
- 윈도우
nslookup
set q=txt
nahida.net
다음은 SSL 통신을 위한 설정이다.
smtpd_tls_cert_file = /etc/letsencrypt/live/nahida.net/fullchain.pem
smtpd_tls_key_file = /etc/letsencrypt/live/nahida.net/privkey.pem
smtpd_tls_security_level = may
smtpd_tls_auth_only = yes
smtp_tls_security_level = may
smtpd_tls_cert_file
,smtpd_tls_key_file
은 사용하는 tls 인증서의 위치를 작성한다. 위 예시는 let’s encrypt 인증서를 사용했다.
Question : 폴더명이 왜
smtp.nahida.net
이 아님?
Answer : 미개한 webroot 방식이 아닌 개쩌는 도메인 인증을 받아서 그럼. 이거는 바로*.nahida.net
으로 모든 서브도메인에서 쓸 수 있는 인증서 발급해줌.
smtpd_tls_security_level
은 may, encrypt중 하나를 선택할 수 있는데 각 의미는 다음과 같다.- may : smtp client에 STARTTLS 지원을 알리지만 tls 연결을 반드시 요구하지는 않음 (Oppotunistic TLS, Implicit TLS)
- encrypt : STARTTLS 지원을 알리고 반드시 tls 연결을 해야만 함(Mandatory TLS, Explicit TLS)
옵션에 대한 설명만 보면 encrypt가 보안상 유리하겠지만 RFC3487에 의하면 내부망이 아닌 인터넷 망에서는 encrypt를 적용하지 말라고 되어있기에 may를 사용한다. 이는 tls를 지원하지 않는 smtp서버도 우리의 smtp 서버로 메일을 전송할 수 있어야 함을 의미한다.
다만 당연히 tls 연결을 지원하지 않는 서버는 대부분 스팸메일 서버이고 이들은 스팸필터에서 걸러진다. 때문에 네이버 메일이나 지메일의 경우 위 표준을 무시하고 mandatory tls 연결만을 허용한다. 이 경우에는 STARTTLS를 하지 않고 MAIL FROM을 진행하면
430 4.7.0 Must issue a STARTTLS command first
에러가 나며 요청을 거부한다.
smtpd_tls_auth_only
는 SASL 인증을 tls 연결중에만 지원한다는 뜻이다. 즉, STARTTLS 명령어를 통해 tls handshake가 진행되지 않은 상태에서는 smtp통신간 AUTH 명령어가 거부된다. 이는 smtp 인증을 위한 username과 password가 평문으로 전송되지 않도록 하는 조치이다.
이는 SMTP submission을 반드시 STARTTLS를 거치도록 한다는 뜻이다. submission을 위해서는 반드시
AUTH
명령어로 서버에 사용자 인증을 거쳐야 하는데, 위 설정으로 인해AUTH
명령어는STARTTLS
이후에 활성화된다.
smtp_tls_security_level
은 메일을 발송할 때(즉 postfix가 SMTP Client의 역할을 수행할 때) TLS 연결에 대한 옵션이다. 즉 메일을 송신할 때 STARTTLS를 하지 않고도 메일을 보낼 수 있도록 지정한 것이다.
Question: postfix 옵션에서 smtp와 smtpd의 차이는?
Answer:
- smtpd : 수신 메일의 관리 및 내부 라우팅 관리
- smtp : 발송 메일의 관리
다음은 TLS handshake간 허용할 프로토콜과 암호화 방식에 대한 설정이다.
smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
tls_preempt_cipherlist = yes
tls_ssl_options = NO_RENEGOTIATION
smtp_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtpd_tls_protocols
는 지원할 프로토콜에 관한 리스트이다. 프로토콜 앞에!
를 붙여 해당 프로토콜을 거부한다고 명시했다. 즉 TLSv1.2 이상의 프로토콜만을 허용한다는 뜻이다.tls_preempt_cipherlist = yes
는 암호화 리스트(어떤 암호화 방식을 사용할 지 선호도순으로 정렬한 리스트)를 클라이언트(MUA)가 아닌 서버(postfix)가 결정한다는 뜻이다. 클라이언트의 암호화 리스트의 정렬순서를 신뢰할 수 없다고 판단할 경우 이 값을 yes로 부여한다.tls_ssl_options
는 OpenSSL의 옵션을 지정한다.NO_RENEGOTIATION
는 연결이 수립된 이후에는 재연결을 거부한다는 뜻이다. DDoS 공격을 완화시키기 위해 사용했다. 참고smtp_tls_protocols
는 postfix가 SMTP Client로 동작할 때의smtpd_tls_protocols
설정이다.
만일
smtpd_tls_security_level = encrypt
로 설정했다면,smtpd_tls_protocols
옵션의 설정값은 무시된다. 이 경우에는smtpd_tls_mandatory_protocols
옵션을 통해 설정해 주어야 한다.
다음은 postfix의 AUTH에 대한 설정이다.
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes
postfix는 SASL 인증을 자체적으로 지원하지 않는다. 따라서 SASL 인증을 수행하는 타 구현체에 해당 구현을 위임해야한다. postfix는 Cyrus SASL과 dovecot로의 위임을 지원한다. 우리는 그 중 dovecot을 위임 구현체로 사용한다.
smtpd_sasl_type
은 SASL 인증을 dovecot으로 수행한다는 뜻이다. 즉, dovecot에 인증을 위임한다.smtpd_sasl_path
은 dovecot과 통신하기 위한 socket의 path를 지정한다. 상대경로로 지정하였고 절대경로/var/spool/postfix/private/auth
를 의미한다.smtpd_sasl_auth_enable
는 postfix가 SASL 인증을 수행하도록 지시한다. 기본적으로 postfix는 SASL 인증(AUTH 명령어) 없이 메일 전송이 가능하다. 이 경우, 해당 SMTP 서버가 인터넷 망에 연결되어 있다면 Open Relay 공격에 취약하기에 인트라넷망, 로컬망이 아닌 경우 반드시 yes로 설정한다.
다음은 postfix와 가상메일(DB) 연동에 대한 설정이다.
virtual_mailbox_domains = mysql:/etc/postfix/virtual.d/domains.cf
virtual_mailbox_maps = mysql:/etc/postfix/virtual.d/mailboxes.cf
virtual_alias_maps = mysql:/etc/postfix/virtual.d/aliases.cf
- 세 설정 모두 가상메일 쿼리문에서 생성한 쿼리문 파일의 위치를 지정한다.
다음은 postfix로 수신된 메일의 처리에 대한 설정이다.
virtual_transport=lmtp:unix:private/dovecot-lmtp
postfix로 수신된 메일에는 다음 두 종류가 있다.
- 타 MTA에서 relay되어 우리 MTA(postfix)로 수신된 메일
- MUA에서 submission하여 우리 MTA(postfix)로 수신된 메일
2번 항목에서 알 수 있듯이 우리 메일서버에서 메일을 보내는 사용자가 우리의 smtp 서버로 메일을 보내는 것 또한 smtp 서버 입장에서는 메일의 수신이다(submission port로 수신한 메일). 이렇게 수신 받은 메일을 타 smtp 서버로 postfix가 relay하여 재발송하는 개념이다.
메일서버는 위의 두 경우에 대해 1.은 받은 메일함으로, 2. 는 보낸 메일함으로 메일을 메일함으로 분류해야 할 책임이 있다. 하지만 postfix는 smtp 서버이기에 이러한 기능을 수행하지 못하고, 때문에 메일함을 관리하는 imap 서버인 dovecot으로 메일을 재전송한다. 이를 위해 dovecot과 unix socket으로 상호연결하고 lmtp 프로토콜로 postfix에서 dovecot으로 메일을 전송한다.
위 경로도 상대경로로 지정하였다. 위 경로에 대한 절대경로는
/var/spool/postfix/private/dovecot-lmtp
이다.
curl https://rspamd.com/rpm-stable/centos-9/rspamd.repo > /etc/yum.repos.d/rspamd.repo
rpm --import https://rspamd.com/rpm-stable/gpg.key
dnf update
dnf install rspamd
rspamadm dkim_keygen -b 2048 -d nahida.net -s kawaii2309 -k /var/lib/rspamd/dkim/nahida.net.kawaii2309.key
clamav
답글 남기기