diff --git a/AUTHORS b/AUTHORS
index 4c19098a6a83787c0ba005666b8551dfc1ebcd62..16ae5e9566e2d990ce708cbc7b2d9d983ce9a1c9 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -32,6 +32,7 @@ to add your name to it.
 - Konstantin Osipov
 - Maksim Kaitmazian
 - Roman Khait
+- Roman Kuzmin
 - Sergey Vustin
 - Valentin Syrovatskiy
 - Vartan Babayan
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7f79b0c99e366746619da87e1e5a9ead1b63787f..b2647bafe023da703f25ce62309f33a061d59615 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -69,6 +69,16 @@ users must specify the expected type of the parameter:
 
 - Ability to change cluster properties via SQL `ALTER SYSTEM` command
 
+- Mutual TLS authentication for Pgproto. 
+
+    1. Set `instance.pg.ssl` configuration parameter to `true`
+    1. Put PEM-encoded `ca.crt` file into instance's data directory along with `server.crt` and `server.key`.
+
+  As a result pgproto server will only accept connection if client has presented a certificate 
+  which was signed by `ca.crt` or it's derivatives.
+
+  If `ca.crt` is absent in instance's data directory, then client certificates are not requested and not validated.
+
 ### Compatibility
 
 - New index for the system table `_pico_replicaset` - `_pico_replicaset_uuid`
diff --git a/src/pgproto.rs b/src/pgproto.rs
index 07e8f812c72015de493ec8e61e00a6470a70467e..0082badf086f7f99e1ae6838344a505101b5beff 100644
--- a/src/pgproto.rs
+++ b/src/pgproto.rs
@@ -114,12 +114,28 @@ impl Context {
             .map_err(Error::invalid_configuration)?;
 
         let addr = (host, port);
-        tlog!(Info, "starting postgres server at {:?}...", addr);
-        let server = server::new_listener(addr)?;
+
+        let tls_note = match &tls_acceptor {
+            Some(acceptor) => {
+                if acceptor.mtls() {
+                    " with mTLS"
+                } else {
+                    " with TLS"
+                }
+            }
+            _ => "",
+        };
+
+        tlog!(
+            Info,
+            "starting postgres server at {:?}{}...",
+            addr,
+            tls_note
+        );
 
         Ok(Self {
-            server,
             tls_acceptor,
+            server: server::new_listener(addr)?,
         })
     }
 }
diff --git a/src/pgproto/tls.rs b/src/pgproto/tls.rs
index bb57160477603d08d3726c43efda16d1a52b68fa..45eb9fb199e882152d8b4e0a75191d3d3ff08626 100644
--- a/src/pgproto/tls.rs
+++ b/src/pgproto/tls.rs
@@ -1,7 +1,11 @@
+use openssl::ssl::SslVerifyMode;
+use openssl::x509::store::X509StoreBuilder;
+use openssl::x509::X509;
 use openssl::{
     error::ErrorStack,
     ssl::{self, HandshakeError, SslFiletype, SslMethod, SslStream},
 };
+
 use std::{fs, io, path::Path, path::PathBuf, rc::Rc};
 use thiserror::Error;
 
@@ -24,12 +28,17 @@ pub enum TlsConfigError {
 
     #[error("key file error '{0}': {1}")]
     KeyFile(PathBuf, std::io::Error),
+
+    #[error("ca file error '{0}': {1}")]
+    CaFile(PathBuf, std::io::Error),
 }
 
 #[derive(Debug)]
 pub struct TlsConfig {
     cert: PathBuf,
     key: PathBuf,
+    // Optional CA certificate for peer certificates verification (mTLS).
+    ca_cert: Option<PathBuf>,
 }
 
 impl TlsConfig {
@@ -42,9 +51,16 @@ impl TlsConfig {
         let key = data_dir.join("server.key");
         let key = fs::canonicalize(&key).map_err(|e| TlsConfigError::KeyFile(key, e))?;
 
+        let ca_cert = data_dir.join("ca.crt");
+        let ca_cert = match fs::canonicalize(&ca_cert) {
+            Ok(path) => Some(path),
+            Err(e) if e.kind() == io::ErrorKind::NotFound => None,
+            Err(e) => Err(TlsConfigError::CaFile(ca_cert, e))?,
+        };
+
         // TODO: Make sure that the file permissions are set to 0640 or 0600.
         // See https://www.postgresql.org/docs/current/ssl-tcp.html#SSL-SETUP for details.
-        Ok(Self { key, cert })
+        Ok(Self { key, cert, ca_cert })
     }
 }
 
@@ -58,6 +74,18 @@ impl TlsAcceptor {
         let mut builder = ssl::SslAcceptor::mozilla_intermediate_v5(SslMethod::tls())?;
         builder.set_certificate_chain_file(&config.cert)?;
         builder.set_private_key_file(&config.key, SslFiletype::PEM)?;
+
+        if let Some(path) = &config.ca_cert {
+            let pem = fs::read(path).map_err(|e| TlsConfigError::CaFile(path.clone(), e))?;
+            let mut store_builder = X509StoreBuilder::new()?;
+            store_builder.add_cert(X509::from_pem(&pem)?)?;
+            builder.set_verify_cert_store(store_builder.build())?;
+
+            let mut verify_mode = SslVerifyMode::PEER;
+            verify_mode.insert(SslVerifyMode::FAIL_IF_NO_PEER_CERT);
+            builder.set_verify(verify_mode);
+        }
+
         Ok(Self(builder.build().into()))
     }
 
@@ -75,4 +103,8 @@ impl TlsAcceptor {
             _ => TlsHandshakeError::HandshakeFailure,
         })
     }
+
+    pub fn mtls(&self) -> bool {
+        self.0.context().verify_mode().contains(SslVerifyMode::PEER)
+    }
 }
diff --git a/test/conftest.py b/test/conftest.py
index c0dfb6c4810699b1534de7d8b7f764b5fd993951..ff56c991bc9ab16eae80dffd750dc77ebe4b1c73 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -2237,12 +2237,13 @@ class AuditServer:
 class Postgres:
     cluster: Cluster
 
-    def __init__(self, cluster: Cluster, ssl: bool = False):
+    def __init__(self, cluster: Cluster, ssl: bool = False, ssl_verify: bool = False):
         # use random port in order to avoid "cannot assign requested address" error
         self.port = random.randint(2000, 30000)
         self.host = "localhost"
         self.cluster = cluster
         self.ssl = ssl
+        self.ssl_verify = ssl_verify
 
     def install(self):
         self.cluster.set_config_file(
@@ -2262,6 +2263,10 @@ instance:
         instance_dir.mkdir()
         shutil.copyfile(ssl_dir / "server.crt", instance_dir / "server.crt")
         shutil.copyfile(ssl_dir / "server.key", instance_dir / "server.key")
+
+        if self.ssl_verify:
+            shutil.copyfile(ssl_dir / "root.crt", instance_dir / "ca.crt")
+
         self.cluster.deploy(instance_count=1)
         return self
 
@@ -2278,3 +2283,8 @@ def postgres(cluster: Cluster):
 @pytest.fixture
 def postgres_with_tls(cluster: Cluster):
     return Postgres(cluster, ssl=True).install()
+
+
+@pytest.fixture
+def postgres_with_mtls(cluster: Cluster):
+    return Postgres(cluster, ssl=True, ssl_verify=True).install()
diff --git a/test/pgproto/ssl_test.py b/test/pgproto/ssl_test.py
index 008b742ceb0c29a8779c0832fcfcd1699568b0a3..2e9752692c99c0df5a355d94528a9c70136ec202 100644
--- a/test/pgproto/ssl_test.py
+++ b/test/pgproto/ssl_test.py
@@ -45,22 +45,53 @@ def test_ssl_refuse(postgres: Postgres):
 
 
 def test_ssl_accept(postgres_with_tls: Postgres):
-    # where the server should find .crt and .key files
-    instance = postgres_with_tls.instance
-    host = postgres_with_tls.host
-    port = postgres_with_tls.port
+    conn = psycopg.connect(prepare_with_tls(postgres_with_tls, ""))
+    conn.close()
+
+
+def test_mtls_with_known_cert(postgres_with_mtls: Postgres):
+    conn = psycopg.connect(prepare_with_tls(postgres_with_mtls, "server"))
+    conn.close()
+
+
+def test_mtls_without_client_cert(postgres_with_mtls: Postgres):
+    with pytest.raises(
+        psycopg.OperationalError,
+        match="(certificate required)|(Connection refused)",
+    ):
+        conn = psycopg.connect(prepare_with_tls(postgres_with_mtls, ""))
+        conn.close()
+
 
+def test_mtls_with_unknown_cert(postgres_with_mtls: Postgres):
+    with pytest.raises(
+        psycopg.OperationalError,
+        match="(unknown ca)|(Connection refused)",
+    ):
+        conn = psycopg.connect(prepare_with_tls(postgres_with_mtls, "self-signed"))
+        conn.close()
+
+
+def prepare_with_tls(pg: Postgres, client_tls_pair_name: str):
+    instance = pg.instance
+    host = pg.host
+    port = pg.port
     user = "user"
     password = "P@ssw0rd"
+    connection_string = f"\
+            user = {user} \
+            password={password} \
+            host={host} \
+            port={port} \
+            sslmode=require"
+
     instance.sql(f"CREATE USER \"{user}\" WITH PASSWORD '{password}' USING md5")
 
-    # where the client should find his certificate
-    test_dir = Path(os.path.realpath(__file__)).parent
-    client_cert_file = test_dir.parent / "ssl_certs" / "root.crt"
-    os.environ["SSL_CERT_FILE"] = str(client_cert_file)
+    if client_tls_pair_name != "":
+        ssl_dir = Path(os.path.realpath(__file__)).parent.parent / "ssl_certs"
+        client_cert_path = ssl_dir / (client_tls_pair_name + ".crt")
+        client_key_path = ssl_dir / (client_tls_pair_name + ".key")
+        connection_string += f" sslcert={client_cert_path} sslkey={client_key_path}"
+        os.chmod(client_key_path, 0o600)
 
-    os.environ["PGSSLMODE"] = "require"
-    conn = psycopg.connect(
-        f"user = {user} password={password} host={host} port={port} sslmode=require"
-    )
-    conn.close()
+    return connection_string
diff --git a/test/ssl_certs/self-signed.crt b/test/ssl_certs/self-signed.crt
new file mode 100644
index 0000000000000000000000000000000000000000..2fa5270c15f0801269ab07e47d6c90ffe1e51a68
--- /dev/null
+++ b/test/ssl_certs/self-signed.crt
@@ -0,0 +1,32 @@
+-----BEGIN CERTIFICATE-----
+MIIFbTCCA1WgAwIBAgIUOmG5dwuSKo9fXeqJnJ93KVYElM4wDQYJKoZIhvcNAQEL
+BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
+GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAgFw0yNDA4MzAyMDA0MTRaGA8yMTI0
+MDgwNjIwMDQxNFowRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx
+ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCAiIwDQYJKoZIhvcN
+AQEBBQADggIPADCCAgoCggIBAJPapiAtAi/s/H0M/cw/j7wg5ZSovprLFggteNjK
+Ebr2KXLySG0NqmyPrYedv2aRnl6oKEZP60nf4j+qHi5wK9FuCg/+gKWyKGBDaYzU
+76gJGDONWHlG972uV/Gu+Li4vUXi5kjnFJV79xMF1G9DDvqhA1Y9g4+LaqzfuXrW
+7DuUzuTDII6/1NJrvFyr6PcGWgU4SdE9UByev+QbV55X+bWRnDW7DbJZXoCzi+Vr
+odewzs2K8TxIJAYxB2TjUCyLMDPMXBnIMFS7XSqXDz/d3fytrsloZC1qumFGWKBj
+YgpFUP9h9QWr3ddhk6fXDr6KgZeXjpqsBn33v0xhJ42JLmBpuH5JWlvzQ0U9xYiY
+ODbWJeseUjV1dC1WLlXopBmlxIlABEQFGIXSracWaxFrl13ZgvoAutCJB5jSUzEl
+j/Z7h72ju8vBNsgdtuQmdrqQolsY7gXUObdYqr/uaSP+VUZUSl0/DrvIJlGW8IyZ
+WlnWxtwRY0RGC58wO3LG6S/MLgeE8houepA71h7PHctcohAbGYIYcKjqs7rVDewJ
+kMQmxqSBO4n8Wyhyuyc3A7qfcfUOUH0h8tX8cA78NEqG9MphCt0DXXcnTrec7yWh
+33pAUXuERJFHgP1ndoKWGwK1SpxTsIeoW/h6Z/HD+o8cJnQaVhtD984Fh2i+s58K
+yju/AgMBAAGjUzBRMB0GA1UdDgQWBBQ8bB2fI79fWCEo8cTPPAT841dtRTAfBgNV
+HSMEGDAWgBQ8bB2fI79fWCEo8cTPPAT841dtRTAPBgNVHRMBAf8EBTADAQH/MA0G
+CSqGSIb3DQEBCwUAA4ICAQCDsGZMaOLUDg85fqlNGiPcm41p+pF+rV9ZMlrzu76Y
+Yz4WdGC5NCmoMfsaawHf6nQ+jj5D1xt9UxmCKdNHg8+9ufa26sOpFM4GcEbQC+B1
+hymbNuPhHXBd4+N9HwbzeK45UfUt6pC3IVk+qW7EAoXkFx4cMHbjsWWMBdfpn4HV
+pAhxVLI8QT9BCyUMmc6ZxTwPjaSQ/t8IsWMRbkGw43jZBok6KOj5NvEbOD6sZ9VR
+fjpF28TwA3RykjMVMOdZ5kBFmCBJQMx8rY5TSkYAIXffRl9Vw+NmUkq/DaHkRNTp
+bsOByh/OzjTQQgy6PbwXKdUdpRBRW4voRJWyGtNiGkEXKSdbXHQpZ4R+7uD3Lp7h
+MX8BswvGVCftVhXsbt1Y1/h3XU8qkF3V4ctw6QAmq2HpKCkVXBiVLPRa4NvfFsEh
+3dsqoohXm2ZtSWP7X5ybzUsZyek/NCaUBA9XjnmD0AlZ8VpEVeqwqLRLEN3vct/r
+4ZfgPz0bDwizP6Pj4B26h9Wz6GnH6ZXJT4cr+HPpQ+nzjaTV6HrvT6t6yKFOOAIQ
+sXZEIV/HEYj/gLfvoSyvR4zVg9IrF5vNbQiZS7H8Fixxuhvj/tvdC5ClnQzjlRkF
+ZBCZsJB5UR7L/FUIuCvoZXdYp2+uLWAnsY+MVkbwTz9IiEDDfyDkmbTspdhHuJCt
+bg==
+-----END CERTIFICATE-----
diff --git a/test/ssl_certs/self-signed.key b/test/ssl_certs/self-signed.key
new file mode 100644
index 0000000000000000000000000000000000000000..0ed4774f4d26939c6062fabfa4dd409b41214ff1
--- /dev/null
+++ b/test/ssl_certs/self-signed.key
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+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
+aYNDIzyuWaDLTxMd73PDZ8m3NJpW
+-----END PRIVATE KEY-----