Skip to content

Commit 4555a0b

Browse files
committed
Implemented mTLS
1 parent 0699dbc commit 4555a0b

12 files changed

Lines changed: 296 additions & 41 deletions

README.md

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,58 @@
44
## Run the following commands to run the project:
55

66
```bash
7-
python -m venv .venv
8-
. .venv/bin/activate
9-
pip install -r requirements.txt
10-
python -m app.main
7+
docker compose up
118
```
9+
10+
This will start the project on 'http://localhost:6502'
11+
12+
13+
14+
# Using mTLS
15+
16+
To use/test mTLS, you need to setup the following:
17+
18+
first, the server is configured with server name "prs", meaning you need to add this to your /etc/hosts (or equivalent):
19+
20+
```
21+
127.0.0.1 prs
22+
```
23+
24+
Next, since the prs certificate is signed with our own development uzi ca cert, you need to import (temporarily) the uzi
25+
ca cert into your browser. Normally, this is done via pkcs12:
26+
27+
```
28+
$ openssl pkcs12 -export -out secrets/uzi-server-ca.p12 -inkey uzi-server-ca.key -in uzi-server-ca.crt
29+
```
30+
It will ask for a password, you can use anything.
31+
Then import the uzi.p12 file into your browser. Again, the password is being asked.
32+
33+
At this point, you would be able to load the mTLS version on https://prs:6503. The connection should be secure as the
34+
browser has the correct CA for the server certificate.
35+
36+
You probably get asked for a client certificate. Here you can use a client certificate that is signed by the uzi ca.
37+
For this you can use either `prs-client-1` or `prs-client-2` (same types). Note that you might need to import these
38+
into your browser through pkcs12 again:
39+
40+
```
41+
$ openssl pkcs12 -export -out secrets/prs-client-1.p12 -inkey prs-client-1.key -in prs-client-1.crt
42+
$ openssl pkcs12 -export -out secrets/prs-client-2.p12 -inkey prs-client-2.key -in prs-client-2.crt
43+
```
44+
45+
If you weren't asked for a client certificate, make sure the site is secured correctly (you should see a lock in the
46+
url bar). If not, client certs are not asked by the browser.
47+
48+
It's also possible that client certificates are disabled for this site. In firefox, go to: tools | settings | privacy & security |
49+
certificates | view certificates | authentication decicions and delete the entry for the site. This will make the browser
50+
ask for a client certificate again.
51+
52+
53+
## Testing with OV or EV certificates
54+
The easiest way to test for OV or EV certificates is to use the `auth.override_cert` configuration setting in app.conf. However,
55+
if you want to test with apache, you must make sure that the CA for that OV or EV cert is inside the `SSLCACertificateFile`
56+
setting of httpd-ssl.conf.
57+
58+
To do that, concatenate the CA of the EV/OV file (including any intermediate certs) into the file pointed by `SSLCACertificateFile`.
59+
60+
When you now specify a client certificate signed by the CA of the EV/OV cert, the browser should connect and you are only able to
61+
view/interact with certain endpoints.

app.conf.autopilot

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ loglevel=debug
33
keystore=json
44

55
[auth]
6-
override_cert = ./secrets/prs-uzi.crt
6+
override_cert = ./secrets/prs.crt
77
allowed_curves = "curve25519 curve448 secp521r1 secp384r1 secp256r1"
88
min_rsa_bitsize = 2048
99

app/container.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ def container_config(binder: inject.Binder) -> None:
2121

2222
if config.app.keystore == "json":
2323
store = JsonKeyStorage(config.json_keystore.path)
24+
if not store.has_key('BPGK-1'):
25+
store.generate_key('BPGK-1')
26+
if not store.has_key('REK-1'):
27+
store.generate_key('REK-1')
28+
2429
crypto_service = MemoryCryptoService(store)
2530
binder.bind(CryptoService, crypto_service)
2631
elif config.app.keystore == "hsm":

app/middleware/auth_required.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
11
from functools import wraps
22
from typing import Callable, Any, TypeVar
33

4+
from fastapi import HTTPException
45
from starlette.requests import Request
56

67
from app.config import get_config
78
from app.services.tls_service import CertAuthentication, CertAuthentications, CertAuthenticationException, TLSService
89

910
T = TypeVar("T", bound=Callable[..., Any])
1011

12+
SSL_PROXY_HEADER = "x-proxy-ssl_client_cert"
1113

1214
def get_cert_from_request(request: Request) -> bytes:
13-
str_pem = request.headers.get("ssl_client_cert", None)
15+
str_pem = request.headers.get(SSL_PROXY_HEADER, None)
1416

1517
pem = bytes()
1618
if str_pem is not None:
1719
pem = str(str_pem).encode()
1820

19-
2021
# If the client certificate is not provided in the header, check if an override certificate is provided
2122
override_cert = get_config().auth.override_cert
22-
if override_cert is not None:
23+
if override_cert is not None and override_cert != "":
2324
with open(override_cert, "r") as file:
2425
pem = file.read().encode()
2526

@@ -41,13 +42,13 @@ async def wrapper(request: Request, *args, **kwargs) -> Any: # type: ignore
4142
tls_service = TLSService(get_config().auth.allowed_curves, get_config().auth.min_rsa_bitsize)
4243
cert_type = tls_service.get_certificate_type(pem)
4344
except CertAuthenticationException as e:
44-
return {"error": str(e)}, 403
45+
raise HTTPException(403, f"Client cert is not valid: {e}")
4546

4647
if cert_type not in certs and CertAuthentications.AUTH_ALL not in certs:
47-
return {"error": "No correct client certificate provided"}, 403
48+
raise HTTPException(403, "No correct client certificate provided")
4849

4950
if not tls_service.validate_cert(pem):
50-
return {"error": "Client certificate is not valid"}, 403
51+
raise HTTPException(403, "Provided client certificate is not valid")
5152

5253
return func(request, *args, **kwargs)
5354

docker-compose.yaml

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,37 +10,44 @@ services:
1010
dockerfile: docker/Dockerfile
1111
volumes:
1212
- ./:/src
13-
- ./secrets:/src/secrets
13+
- ./secrets:/usr/local/secrets
1414
ports:
1515
- 6502:6502
1616
depends_on:
17-
- softhsm
1817
- redis
1918
networks:
2019
zmodules:
2120
aliases:
2221
- prs-app
2322

23+
app-ssl:
24+
image: httpd
25+
restart: no
26+
ports:
27+
- 6503:443
28+
volumes:
29+
- ./secrets:/usr/local/secrets
30+
- ./docker/httpd.conf:/usr/local/apache2/conf/httpd.conf
31+
- ./docker/httpd-ssl.conf:/usr/local/apache2/conf/extra/httpd-ssl.conf
32+
depends_on:
33+
- app
34+
networks:
35+
zmodules:
36+
aliases:
37+
- prs-app-ssl
38+
2439
redis:
2540
image: redis:6.2
2641
ports:
2742
- 6379:6379
2843
networks:
2944
- zmodules
3045

31-
softhsm:
32-
image: cremuzzi/softhsm2
33-
volumes:
34-
- ./softhsm.conf:/etc/softhsm2/softhsm2.conf
35-
- ./tokens:/var/lib/softhsm/tokens
36-
networks:
37-
- zmodules
38-
3946
networks:
4047
zmodules:
4148
driver: bridge
4249
name: zmodules
4350

4451
volumes:
4552
secrets:
46-
name: secrets
53+
name: secrets

docker/httpd-ssl.conf

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
Listen 443
2+
3+
SSLCipherSuite ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
4+
SSLProxyCipherSuite HIGH:MEDIUM:!MD5:!RC4:!3DES
5+
6+
SSLHonorCipherOrder on
7+
8+
SSLProtocol -all +TLSv1.2 +TLSv1.3
9+
SSLProxyProtocol -all +TLSv1.2 +TLSv1.3
10+
11+
SSLPassPhraseDialog builtin
12+
13+
SSLSessionCache "shmcb:/usr/local/apache2/logs/ssl_scache(512000)"
14+
SSLSessionCacheTimeout 300
15+
16+
<VirtualHost _default_:443>
17+
DocumentRoot "/usr/local/apache2/htdocs"
18+
ServerName prs
19+
ServerAdmin you@example.com
20+
ErrorLog /proc/self/fd/2
21+
TransferLog /proc/self/fd/2
22+
23+
SSLEngine on
24+
25+
SSLCertificateFile "/usr/local/secrets/prs.crt"
26+
SSLCertificateKeyFile "/usr/local/secrets/prs.key"
27+
SSLCertificateChainFile "/usr/local/secrets/uzi-server-ca.crt"
28+
29+
SSLCACertificateFile "/usr/local/secrets/uzi-server-ca.crt"
30+
SSLVerifyClient require
31+
SSLVerifyDepth 3
32+
SSLOptions +ExportCertData
33+
<FilesMatch "\.(cgi|shtml|phtml|php)$">
34+
SSLOptions +StdEnvVars
35+
</FilesMatch>
36+
<Directory "/usr/local/apache2/cgi-bin">
37+
SSLOptions +StdEnvVars
38+
</Directory>
39+
40+
BrowserMatch "MSIE [2-5]" \
41+
nokeepalive ssl-unclean-shutdown \
42+
downgrade-1.0 force-response-1.0
43+
CustomLog /proc/self/fd/1 \
44+
"%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b"
45+
46+
</VirtualHost>

docker/httpd.conf

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
ServerRoot "/usr/local/apache2"
2+
3+
Listen 80
4+
5+
LoadModule mpm_event_module modules/mod_mpm_event.so
6+
LoadModule authn_file_module modules/mod_authn_file.so
7+
LoadModule authn_core_module modules/mod_authn_core.so
8+
LoadModule authz_host_module modules/mod_authz_host.so
9+
LoadModule authz_groupfile_module modules/mod_authz_groupfile.so
10+
LoadModule authz_user_module modules/mod_authz_user.so
11+
LoadModule authz_core_module modules/mod_authz_core.so
12+
LoadModule access_compat_module modules/mod_access_compat.so
13+
LoadModule auth_basic_module modules/mod_auth_basic.so
14+
LoadModule socache_shmcb_module modules/mod_socache_shmcb.so
15+
LoadModule reqtimeout_module modules/mod_reqtimeout.so
16+
LoadModule filter_module modules/mod_filter.so
17+
LoadModule mime_module modules/mod_mime.so
18+
LoadModule log_config_module modules/mod_log_config.so
19+
LoadModule env_module modules/mod_env.so
20+
LoadModule headers_module modules/mod_headers.so
21+
LoadModule setenvif_module modules/mod_setenvif.so
22+
LoadModule version_module modules/mod_version.so
23+
LoadModule proxy_module modules/mod_proxy.so
24+
LoadModule proxy_http_module modules/mod_proxy_http.so
25+
LoadModule proxy_balancer_module modules/mod_proxy_balancer.so
26+
LoadModule slotmem_shm_module modules/mod_slotmem_shm.so
27+
LoadModule ssl_module modules/mod_ssl.so
28+
LoadModule proxy_http2_module modules/mod_proxy_http2.so
29+
LoadModule unixd_module modules/mod_unixd.so
30+
LoadModule status_module modules/mod_status.so
31+
LoadModule autoindex_module modules/mod_autoindex.so
32+
LoadModule dir_module modules/mod_dir.so
33+
LoadModule alias_module modules/mod_alias.so
34+
35+
<IfModule unixd_module>
36+
User www-data
37+
Group www-data
38+
</IfModule>
39+
40+
ServerAdmin you@example.com
41+
42+
<Directory />
43+
AllowOverride none
44+
Require all denied
45+
</Directory>
46+
47+
DocumentRoot "/usr/local/apache2/htdocs"
48+
<Directory "/usr/local/apache2/htdocs">
49+
Options Indexes FollowSymLinks
50+
AllowOverride None
51+
Require all granted
52+
</Directory>
53+
54+
<IfModule dir_module>
55+
DirectoryIndex index.html
56+
</IfModule>
57+
58+
<Files ".ht*">
59+
Require all denied
60+
</Files>
61+
62+
ErrorLog /proc/self/fd/2
63+
64+
LogLevel warn
65+
66+
<IfModule log_config_module>
67+
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
68+
LogFormat "%h %l %u %t \"%r\" %>s %b" common
69+
70+
<IfModule logio_module>
71+
# You need to enable mod_logio.c to use %I and %O
72+
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio
73+
</IfModule>
74+
75+
CustomLog /proc/self/fd/1 common
76+
77+
</IfModule>
78+
79+
<IfModule alias_module>
80+
ScriptAlias /cgi-bin/ "/usr/local/apache2/cgi-bin/"
81+
</IfModule>
82+
83+
<IfModule cgid_module>
84+
</IfModule>
85+
86+
<Directory "/usr/local/apache2/cgi-bin">
87+
AllowOverride None
88+
Options None
89+
Require all granted
90+
</Directory>
91+
92+
<IfModule headers_module>
93+
RequestHeader unset Proxy early
94+
</IfModule>
95+
96+
<IfModule mime_module>
97+
TypesConfig conf/mime.types
98+
AddType application/x-compress .Z
99+
AddType application/x-gzip .gz .tgz
100+
101+
</IfModule>
102+
103+
<IfModule proxy_html_module>
104+
Include conf/extra/proxy-html.conf
105+
</IfModule>
106+
107+
Include conf/extra/httpd-ssl.conf
108+
109+
<IfModule ssl_module>
110+
SSLRandomSeed startup builtin
111+
SSLRandomSeed connect builtin
112+
</IfModule>
113+
114+
ProxyPass / http://app:6502/
115+
RequestHeader set X-Proxy-SSL_CLIENT_VERIFY "%{SSL_CLIENT_VERIFY}s"
116+
RequestHeader set X-Proxy-SSL_CLIENT_CERT "%{SSL_CLIENT_CERT}s"

docker/init.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ echo "➡️ Generating TLS certificates"
66
if [ -e secrets/ssl/server.key ] && [ -e secrets/ssl/server.cert ]; then
77
echo "⚠️ TLS certificates already exist. Skipping."
88
else
9+
./tools/download_certs.sh
910
./tools/generate_certs.sh
1011
fi
1112

softhsm.conf

Whitespace-only changes.

tools/download_certs.sh

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/usr/bin/env bash
2+
3+
set -e
4+
5+
SECRETS_DIR=secrets
6+
7+
echo "Downloading generated certs from secrets container"
8+
9+
response=$(curl --write-out '%{http_code}' --output /dev/null http://secrets/README.md)
10+
11+
if [[ "$response" -ne 200 ]] ; then
12+
echo "Unable to download secrets from secrets container"
13+
echo "Did you start the docker compose project from the coordination repo?"
14+
exit 1
15+
fi
16+
17+
curl -f http://secrets/prs/prs.crt -o $SECRETS_DIR/prs.crt
18+
curl -f http://secrets/prs/prs.key -o $SECRETS_DIR/prs.key
19+
curl -f http://secrets/prs-client-1/prs-client-1.crt -o $SECRETS_DIR/prs-client-1.crt
20+
curl -f http://secrets/prs-client-1/prs-client-1.key -o $SECRETS_DIR/prs-client-1.key
21+
curl -f http://secrets/prs-client-2/prs-client-2.crt -o $SECRETS_DIR/prs-client-2.crt
22+
curl -f http://secrets/prs-client-2/prs-client-2.key -o $SECRETS_DIR/prs-client-2.key
23+
curl -f http://secrets/uzi-server-ca.crt -o $SECRETS_DIR/uzi-server-ca.crt
24+
25+
echo "Downloaded generated certs from secrets container"

0 commit comments

Comments
 (0)