diff --git a/Dockerfile b/Dockerfile
index f0412c867848ef2320b644ec53981b5a14786b33..ef933fd5ca6106ddbe8ed5bc11bedc52cbfdfa81 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,8 +1,9 @@
 FROM bitnami/minideb:stretch
 
 COPY build.sh /tmp/build.sh
-#COPY gunicorn.conf /etc/gunicorn.conf
+COPY utils /tmp/utils
 
 RUN /tmp/build.sh && rm /tmp/build.sh
 
+# Example entrypoint for child images:
 #ENTRYPOINT ["/usr/sbin/gunicorn", "--worker-class=gevent", "--workers=2", "-c", "/etc/gunicorn.conf", "app:app"]
diff --git a/README.md b/README.md
index b8f0c1455f19b35ac4ce88abd70208398737772e..ae50ed9dc556007bf43e1d3c815d2c45016014f9 100644
--- a/README.md
+++ b/README.md
@@ -6,3 +6,11 @@ The configuration for the app should be copied by sub-images to /etc/gunicorn.co
 (even though it is a Python file). The name of the application module to run
 should be passed as part of the ENTRY\_POINT set by the child image.
 
+### TLS Authentication
+
+This image installs a bit of Python code, required to use TLS
+authentication for your application in a standardized fashion (since
+it requires a custom GUnicorn worker).
+
+To read how to do so, check the [utils/README.md](utils/README.md)
+file.
diff --git a/build.sh b/build.sh
index f85d1985024f3cc5d8491c4a35d8d012cf62f045..ed5a9c987d867c8c296dbae73e37ee4f7b52df80 100755
--- a/build.sh
+++ b/build.sh
@@ -25,6 +25,9 @@ set -x
 install_packages ${PACKAGES} \
     || die "could not install packages"
 
+# Set up the ai3_gunicorn_utils package.
+pip install /tmp/utils
+
 # Remove packages used for installation.
 #apt-get remove -y --purge ${BUILD_PACKAGES}
 apt-get autoremove -y
diff --git a/utils/README.md b/utils/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..d0c2502210c767cfbd31d1acefd4d892930675a6
--- /dev/null
+++ b/utils/README.md
@@ -0,0 +1,75 @@
+ai3_gunicorn_utils
+===
+
+WSGI middleware and utilities for gunicorn that enable TLS-based
+authorization to secure service-to-service communication, in line with
+the features provided
+by
+[ai3/go-common](https://git.autistici.org/ai3/go-common/blob/master/serverutil/tls.go).
+
+To enable the feature for an application using
+the
+[ai3/docker-gunicorn-base](https://git.autistici.org/ai3/docker-gunicorn-base) image,
+follow these steps:
+
+### Setup GUnicorn
+
+In the *gunicorn.conf* file for your application, set up our custom
+gunicorn worker (ai3_gunicorn_utils.worker.TLSAuthWorker), and
+enable SSL with client certificates:
+
+```yaml
+worker_class: "ai3_gunicorn_utils.worker.TLSAuthWorker"
+keyfile: "/etc/credentials/x509/myservice/server/private_key.pem"
+certfile: "/etc/credentials/x509/myservice/server/cert.pem"
+ca_certs: "/etc/credentials/x509/myservice/ca.pem"
+ssl_version: 5
+cert_reqs: 2
+```
+
+Note that we need to use numeric values for constants in Python's ssl
+module: `ssl.PROTOCOL_TLSv1_2` is 5 above, and `ssl.CERT_REQUIRED` is
+2.
+
+The custom worker is necessary to pass the client certificate
+information to the WSGI middleware.
+
+### Setup your application
+
+Configure your WSGI application file to (optionally) initialize TLS
+authentication if the TLS_AUTH_CONFIG environment variable is defined:
+
+```python
+from ai3_gunicorn_utils.tls_auth import tls_auth_wrap
+from myapplication import create_wsgi_app
+
+application = tls_auth_wrap(create_wsgi_app())
+```
+
+or, if you are using Flask for instance:
+
+```python
+from ai3_gunicorn_utils.tls_auth import init_flask_app
+from myapplication import create_app
+
+application = create_app()
+init_flask_app(application)
+```
+
+### Configure TLS authentication
+
+The file pointed to by TLS_AUTH_CONFIG must be a Python file, defining
+one top-level variable `ACL`, which contains a list of access-control
+patterns. For each requests, at least one of these patterns must match
+the request parameters. ACL patterns are made of two regular
+expressions, one that should match the HTTP request path, another that
+should match the client certificate's subject CN. They are expressed
+as Python dictionaries with *path* and *cn* attributes, for instance:
+
+```python
+ACL = [
+  {"path": "^/api/", "cn": "^other-service$"},
+  {"path": "^/metrics", "cn": ".*"},
+]
+```
+
diff --git a/utils/ai3_gunicorn_utils/__init__.py b/utils/ai3_gunicorn_utils/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..4722b66974062dbcd06a75f733de80e7b97a994f
--- /dev/null
+++ b/utils/ai3_gunicorn_utils/__init__.py
@@ -0,0 +1,4 @@
+
+# The special header that we set in our custom gunicorn worker, that
+# holds the client certificate's subject CN.
+SSL_CLIENT_CN_HEADER = 'X-SSL-Client-CN'
diff --git a/utils/ai3_gunicorn_utils/tls_auth.py b/utils/ai3_gunicorn_utils/tls_auth.py
new file mode 100644
index 0000000000000000000000000000000000000000..a452e11ce5e5c7a5cc3306035766fd7ba48d9ead
--- /dev/null
+++ b/utils/ai3_gunicorn_utils/tls_auth.py
@@ -0,0 +1,61 @@
+import re
+
+from ai3_gunicorn_utils import SSL_CLIENT_CN_HEADER
+WSGI_SSL_CLIENT_CN_HEADER = 'HTTP_' + SSL_CLIENT_CN_HEADER.upper()
+
+
+def _abort(start_response, code, code_str):
+    hdrs = [('Content-Type', 'text/plain')]
+    start_response(code, code_str)
+    return [code_str]
+
+
+def _compile_config(config):
+    # Turn config dictionaries into path/cn compiled regexp tuples.
+    def _compile_entry(e):
+        return (re.compile(e['path']), re.compile(e['cn']))
+    return filter(_compile_entry, config)
+
+
+class TLSAuthMiddleware(object):
+
+    def __init__(self, next_app, config):
+        self._next_app = next_app
+        self._config = _compile_config(config)
+
+    def __call__(self, environ, start_response):
+        path = environ['SCRIPT_NAME'] + environ['PATH_INFO']
+        client_cn = environ.get(WSGI_SSL_CLIENT_CN_HEADER)
+        if not client_cn:
+            return _abort(start_response, 401, 'Unauthorized')
+        for path_rx, cn_rx in self._config:
+            if path_rx.match(path) and cn_rx.match(client_cn):
+                return self._next_app(environ, start_response)
+        return _abort(start_response, 403, 'Forbidden')
+
+
+def tls_auth_wrap(next_app):
+    """Wrap a WSGI application with TLSAuthMiddleware.
+
+    Configuration is loaded from the file specified by the environment
+    variable TLS_AUTH_CONFIG. If the variable is not defined or empty,
+    TLS authentication is disabled.
+
+    The configuration file should be a Python file defining a single
+    ACL variable, a list of {path, cn} dictionaries containing ACL
+    definitions.
+
+    """
+    config_file = os.getenv('TLS_AUTH_CONFIG')
+    if not config_file:
+        return next_app
+
+    config_ctx = {'ACL': []}
+    execfile(config_file, config_ctx)
+    config = config_ctx['ACL']
+    return TLSAuthMiddleware(next_app, config)
+
+
+def init_flask_app(flask_app):
+    """Add TLSAuthMiddleware to a Flask application."""
+    app.wsgi_app = tls_auth_wrap(app.wsgi_app)
diff --git a/utils/ai3_gunicorn_utils/worker.py b/utils/ai3_gunicorn_utils/worker.py
new file mode 100644
index 0000000000000000000000000000000000000000..af1457b6a262a48c6b9b069a6e1486a1ea85c80c
--- /dev/null
+++ b/utils/ai3_gunicorn_utils/worker.py
@@ -0,0 +1,20 @@
+from ai3_gunicorn_utils import SSL_CLIENT_CN_HEADER
+from gunicorn.workers.ggevent import GeventWorker as worker_base
+
+
+class TLSAuthWorker(worker_base):
+    """Custom gunicorn worker that sets SSL_CLIENT_CN_HEADER.
+
+    Based on the gevent worker.
+
+    """
+    def handle_request(self, listener, req, client, addr):
+        subject = dict(client.getpeercert().get('subject')[0])
+
+        # Remove any SSL_CLIENT_CN_HEADER that might have been already
+        # present in the incoming request, and set our own value.
+        headers = filter(lambda x: x[0] != SSL_CLIENT_CN_HEADER, req.headers)
+        headers.append((SSL_CLIENT_CN_HEADER, subject.get('commonName')))
+        req.headers = headers
+
+        super(CustomWorker, self).handle_request(listener, req, client, addr)
diff --git a/utils/setup.py b/utils/setup.py
new file mode 100644
index 0000000000000000000000000000000000000000..73a58e7f1802d3e5aafa2e02034ce2e10a5f1b9b
--- /dev/null
+++ b/utils/setup.py
@@ -0,0 +1,11 @@
+from setuptools import setup, find_packages
+
+setup(
+    name='ai3-gunicorn-utils',
+    version='0.1',
+    description='Misc utilities to support running gunicorn in the ai3 system.',
+    author='A/I',
+    author_email='info@autistici.org',
+    install_requires=[],
+    packages=find_packages(),
+)