.. _security:
================================
Transport security for kernels
================================
.. versionadded:: 8.9
By default, the ZMQ sockets used to communicate with kernels (shell, IOPub,
stdin, control, heartbeat) are bound to local TCP ports with no
transport-level encryption. Any process on the same host that can reach
those ports can connect and read messages, including all IOPub output.
`CurveZMQ `_ adds elliptic-curve
encryption and authentication at the ZMQ transport layer. When enabled, the
``KernelManager`` generates a keypair, writes it into the kernel's connection
file, and configures all sockets as a CurveZMQ server. Clients must present
the correct server public key to connect; unauthenticated connections are
silently dropped before any data is delivered.
.. note::
CurveZMQ is only available when pyzmq was compiled against a libzmq that
includes libsodium. You can verify this with::
python -c "import zmq; print(zmq.has('curve'))"
If this prints ``False``, the ``transport_encryption`` setting has no
effect and attempts to set it to ``'auto'`` or ``'required'`` will raise
a :exc:`traitlets.TraitError`.
.. note::
``transport_encryption`` applies to TCP transport only. IPC sockets
already rely on filesystem permissions for access control and do not
support CurveZMQ.
The ``transport_encryption`` setting
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Transport encryption is controlled by the
``KernelManager.transport_encryption`` traitlet, which accepts three values:
``'disabled'`` (default)
No CurveZMQ keys are generated. All kernel sockets are unencrypted.
``'auto'``
Keys are generated **only when the kernelspec declares support** via
``metadata.supported_encryption: 'curve'``. Kernelspecs that do not
declare this field are started without encryption, so the setting is safe
to enable globally without breaking existing kernels.
``'required'``
Keys are always generated. Startup fails with a :exc:`RuntimeError` if
the kernelspec does not declare ``metadata.supported_encryption: 'curve'``,
so kernels that have not been updated to handle the connection-file keys
are never started unencrypted.
To enable encryption for all kernels that support it, add the following to
your configuration:
.. code:: python
c.KernelManager.transport_encryption = "auto"
To enforce encryption and refuse to start kernels that do not declare support:
.. code:: python
c.KernelManager.transport_encryption = "required"
Kernelspec requirements
~~~~~~~~~~~~~~~~~~~~~~~
A kernel must declare CurveZMQ support in its ``kernel.json`` before the
``KernelManager`` will provision keys for it:
.. code:: JSON
{
"argv": ["python", "-m", "ipykernel_launcher", "-f", "{connection_file}"],
"display_name": "Python 3",
"language": "python",
"metadata": {
"supported_encryption": "curve"
}
}
When ``transport_encryption`` is ``'auto'``, kernelspecs without this field
are started normally without encryption. When it is ``'required'``, their
startup is refused.
.. note::
When updating a previously installed kernel to a version that supports
encryption you may need to re-install the kernelspec or manually add the
``supported_encryption`` metadata field. If you subsequently decide to
downgrade, you will need to remove this field as otherwise the kernel will
silently fail to connect.
Connection file fields
~~~~~~~~~~~~~~~~~~~~~~
When ``transport_encryption`` is active the connection file written to disk
will contain two additional fields alongside the usual port and key fields:
``curve_publickey``
Z85-encoded 40-character ASCII string holding the server's CurveZMQ
public key.
``curve_secretkey``
Z85-encoded 40-character ASCII string holding the server's CurveZMQ
secret key.
Kernel implementations must read these fields from the connection file and
apply them to their ZMQ sockets before binding. See :ref:`kernels` for the
full description of the connection file format.
Implementing curve support in a kernel
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A kernel that wants to be compatible with ``transport_encryption`` must apply
the keypair to every socket it binds. In Python, using pyzmq, that looks like:
.. code:: python
import json
import zmq
with open(connection_file_path) as f:
cfg = json.load(f)
ctx = zmq.Context()
shell = ctx.socket(zmq.ROUTER)
if "curve_publickey" in cfg and "curve_secretkey" in cfg:
shell.curve_secretkey = cfg["curve_secretkey"].encode()
shell.curve_publickey = cfg["curve_publickey"].encode()
shell.curve_server = True
shell.bind(f"tcp://{cfg['ip']}:{cfg['shell_port']}")
The same pattern applies to the IOPub, stdin, control, and heartbeat sockets.
Setting ``curve_server = True`` on a bound socket causes ZMQ to require
CurveZMQ authentication on every incoming connection; unauthenticated clients
are rejected automatically.
Kernels based on `ipykernel `_ v7.3+ handle
this automatically when the connection file contains the curve fields.
.. seealso::
:ref:`kernels`
Connection file format and kernelspec reference.
:ref:`provisioning`
How ``LocalProvisioner`` generates and injects curve keys during
kernel startup.