Skip to content
Open
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
25f1a21
[feature] Extended Template model for standalone X.509 certificates #…
stktyagi May 26, 2026
a795e09
[fix] Updated and added tests Fixes #1356
stktyagi May 26, 2026
b946d26
[fix] Fixed auto_cert help text
stktyagi May 26, 2026
ff1e8a1
Merge branch 'gsoc26-x509-certificate-generator-templates' into issue…
stktyagi May 27, 2026
0053bcf
[fix] Validate cert relations only inside the cert branch #1356
stktyagi May 27, 2026
01221d9
[fix] Added test for cert validation path #1356
stktyagi May 27, 2026
5c551c7
[fix] Fixed flake error #1356
stktyagi May 27, 2026
d6fd193
[feature] Add DeviceCertificate M2M model and cert template support #…
stktyagi May 28, 2026
cae38a5
[fix] Updated test for double checks #1377
stktyagi May 28, 2026
02912a0
[feature] Updated django admin and API #1357 #1361
stktyagi May 29, 2026
3434719
[fix] Fixed tests and improvedd serializers #1357
stktyagi May 29, 2026
298572f
[feature] Implemented certificate lifecycle management #1358
stktyagi May 29, 2026
5c522e9
Merge branch 'gsoc26-x509-certificate-generator-templates' into issue…
stktyagi May 30, 2026
052cebf
[tests] Added tests for cert template type #1358
stktyagi May 29, 2026
148bfd2
[tests] Added tests for API #1361
stktyagi May 30, 2026
badf1c1
[fix] Addressed model improvements #1377
stktyagi May 30, 2026
a692c77
[fix] Fixed certificate template reordering lifecycle #1358
stktyagi May 31, 2026
16703f1
[fix] Minor improvements
stktyagi May 31, 2026
212b839
[fix] Made cert creation transactional
stktyagi Jun 1, 2026
37ff9a9
[fix] Fixed NULL handling in get_unassigned_certs()
stktyagi Jun 1, 2026
ef90a6c
[fix] Filtered revoked certs from blueprint list
stktyagi Jun 2, 2026
b8bc3e0
Merge branch 'gsoc26-x509-certificate-generator-templates' into issue…
stktyagi Jun 2, 2026
88af90a
Merge branch 'gsoc26-x509-certificate-generator-templates' into issue…
stktyagi Jun 3, 2026
bca3809
[fix] Prevent zombie certs
stktyagi Jun 3, 2026
99cd151
[tests] Cover the partial replacement path too
stktyagi Jun 3, 2026
7390020
Merge branch 'gsoc26-x509-certificate-generator-templates' into issue…
stktyagi Jun 4, 2026
ee79c93
[chores] Added custom oid extensions for mac and uuid #1377
stktyagi Jun 4, 2026
c92cc5e
Merge branch 'gsoc26-x509-certificate-generator-templates' into issue…
stktyagi Jun 5, 2026
dfa650c
[docs] Added inital documentation
stktyagi Jun 5, 2026
88003e2
[feature] Added implementation for context injection #1360
stktyagi Jun 6, 2026
b9db466
[tests] Updated tests to pass CI
stktyagi Jun 7, 2026
28f9cce
[fix] Addressed review changes
stktyagi Jun 7, 2026
228e9bc
Merge branch 'gsoc26-x509-certificate-generator-templates' into issue…
stktyagi Jun 9, 2026
1e9e813
[docs] Added docs for context injection #1360
stktyagi Jun 9, 2026
717c3e1
[feature] Implemented certificate regeneration #1359
stktyagi Jun 12, 2026
7f8dba9
[fix] Improved certificate regeneration
stktyagi Jun 12, 2026
e7a6b16
[fix] Synchronize config status on standalone cert renewal
stktyagi Jun 13, 2026
406e780
[docs] Addressed review comments
stktyagi Jun 13, 2026
25ab4c3
[fix] Made hardware drift cert regeneration idempotent
stktyagi Jun 16, 2026
4711fa3
Merge branch 'gsoc26-x509-certificate-generator-templates' into issue…
stktyagi Jun 18, 2026
a819721
[chores] Updated DeviceCertificate model and tests
stktyagi Jun 18, 2026
8c32282
[chores] Minor improvements
stktyagi Jun 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/developer/extending.rst
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ Once you have created the models, add the following to your
CONFIG_TEMPLATE_MODEL = "sample_config.Template"
CONFIG_VPN_MODEL = "sample_config.Vpn"
CONFIG_VPNCLIENT_MODEL = "sample_config.VpnClient"
CONFIG_DEVICECERTIFICATE_MODEL = "sample_config.DeviceCertificate"
CONFIG_ORGANIZATIONCONFIGSETTINGS_MODEL = "sample_config.OrganizationConfigSettings"
CONFIG_ORGANIZATIONLIMITS_MODEL = "sample_config.OrganizationLimits"
CONFIG_WHOISINFO_MODEL = "sample_config.WHOISInfo"
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ the OpenWISP architecture.
user/intro.rst
user/device-config-status.rst
user/templates.rst
user/certificate-templates.rst
user/variables.rst
user/device-groups.rst
user/push-operations.rst
Expand Down
219 changes: 219 additions & 0 deletions docs/user/certificate-templates.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
X.509 Certificate Generator Templates

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add this page to the /docs/index.rst, please also add a mention of this feature in /docs/user/intro.rst.

=====================================

.. contents:: **Table of Contents**:
:depth: 3
:local:

Introduction
------------

A Certificate Template is a specific type of :doc:`Configuration Template
</controller/user/templates>` that allows OpenWISP to centrally manage and
automatically provision X.509 client certificates for devices.

Unlike VPN templates, which generate certificates as part of a larger
tunnel configuration (like OpenVPN or WireGuard), Certificate Templates
are standalone. They are ideal for use cases where devices need
cryptographic identities for external services, such as:

- Mutual TLS (mTLS) authentication against internal APIs.
- Cryptographically signed device identities for 802.1x or captive
portals.
- Secure payload signing.

.. _certificate_templates_setup:

Setting Up a Certificate Template
---------------------------------

To create a Certificate Template, navigate to the Templates section in the
OpenWISP admin and set the **Type** to :guilabel:`Certificate` (``cert``).
This will reveal the certificate-specific configuration fields.

.. image:: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.4/certificate-templates/certificate-template.png
:target: https://raw.githubusercontent.com/openwisp/openwisp-controller/docs/docs/1.4/certificate-templates/certificate-template.png
:alt: Certificate Template admin form

:guilabel:`Certificate Authority` (required)
The Certificate Authority that will sign the X.509 certificates
generated for each device using the template. The CA must belong to
the same organization as the template (or be shared).

:guilabel:`Blueprint Certificate` (optional)
An existing, unassigned, non-revoked X.509 certificate that will be
used as a *blueprint*. The extensions, key length, digest, and other
subject fields of the blueprint are copied to each newly generated
certificate. The blueprint must be signed by the selected
:guilabel:`Certificate Authority` and must not already be bound to a
device.

:guilabel:`Automatic certificate provisioning` (``auto_cert``)
When enabled (which is the default behavior), an X.509 certificate is
automatically created and signed by the template's CA the moment the
template is assigned to a device configuration.

.. _certificate_templates_validation:

Validation Rules
----------------

To ensure cryptographic integrity and prevent misconfigurations,
Certificate Templates enforce the following validation rules upon saving:

- A :guilabel:`Certificate Authority` is strictly required.
- The :guilabel:`Blueprint Certificate` **must** be signed by the selected
:guilabel:`Certificate Authority`. Saving with a mismatched pair will
fail.
- The :guilabel:`Blueprint Certificate` must be *unassigned* (it cannot be
currently bound to any device). The blueprint dropdown in the admin
panel is pre-filtered to show only unassigned, non-revoked certificates.
- Both the :guilabel:`Certificate Authority` and the :guilabel:`Blueprint
Certificate` must belong to the same organization as the template, or be
marked as shared.

.. _certificate_templates_lifecycle:

Provisioning and Revocation Lifecycle
-------------------------------------

Certificate Templates are tightly coupled with the device configuration
lifecycle to ensure certificates are only valid while the device is
actively authorized to use them.

**When assigned to a device:** When a Certificate Template is added to a
device configuration, a ``DeviceCertificate`` relationship is established.
If ``auto_cert`` is enabled, an X.509 certificate is generated and signed
in the same database transaction:

- The certificate's subject and extensions are copied from the
:guilabel:`Blueprint Certificate` (or the CA defaults).
- The certificate is augmented with two custom OpenWISP OIDs that
cryptographically identify the device (see
:ref:`certificate_templates_oid_extensions`).
- The certificate's :guilabel:`Common Name` is generated using the
:ref:`OPENWISP_CONTROLLER_COMMON_NAME_FORMAT
<openwisp_controller_common_name_format>` setting, suffixed with a
unique slug to prevent collisions.

**When removed from a device:** When the Certificate Template is
unassigned from a device configuration, the ``DeviceCertificate``
relationship is deleted. Crucially, the underlying X.509 certificate is
**automatically revoked** (provided it was created via
``auto_cert=True``). This ensures that compromised or decommissioned
devices immediately lose their cryptographic access.

.. _certificate_templates_active_lock:

Active Template Mutation Lock
-----------------------------

To prevent breaking the cryptographic binding with devices that are
already using a template, certain destructive changes are blocked while
the template is assigned to *active* or *activating* device
configurations.

You **cannot** change the following on an actively used template:

- :guilabel:`Type` (only changing a ``cert`` template to a different type
is blocked)
- :guilabel:`Certificate Authority`
- :guilabel:`Blueprint Certificate`

If you need to update these core parameters, you must first unassign the
Certificate Template from all affected device configurations (or
deactivate the devices), apply your template changes, and then reassign
them.

.. _certificate_templates_oid_extensions:

Custom Device Identification OIDs
---------------------------------

To allow external systems to uniquely and securely identify devices by
parsing their X.509 certificates, every automatically generated
certificate includes two custom ASN.1 Object Identifiers (OIDs):

- ``1.3.6.1.4.1.65901.1``: Contains the MAC address of the device
(``ASN1:UTF8:string:<mac_address>``).
- ``1.3.6.1.4.1.65901.2``: Contains the UUID of the device
(``ASN1:UTF8:string:<device_id>``).

These OIDs are appended securely to the certificate in addition to any
extensions inherited from the :guilabel:`Blueprint Certificate`.

.. _certificate_templates_context:

Using Certificates in Configuration (Context Injection)
-------------------------------------------------------

To deploy the certificate to the device, administrators must reference the
automatically generated variables within the template's JSON configuration
payload.

To prevent variable collisions when multiple Certificate Templates are
assigned to a single device, OpenWISP namespaces the Jinja2 variables
using the template's 32-character hexadecimal UUID (the UUID with dashes
removed).

The following variables are dynamically injected into the configuration

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The certificate context section says "the following variables" but omits cert_<template_uuid_hex>_path and cert_<template_uuid_hex>_key_path, which are exposed by get_cert_context() and tested. Please add these two public variables here.

context:

- ``{{ cert_<template_uuid_hex>_pem }}``: The public certificate.
- ``{{ cert_<template_uuid_hex>_key }}``: The private key.
- ``{{ cert_<template_uuid_hex>_uuid }}``: The UUID of the generated
certificate.

**Workflow Example:**

1. Create a Certificate Template and click **Save and continue editing**
to generate its UUID.
2. Note the UUID from the URL (e.g.,
``d7d20eb8-b3c8-420c-9103-401e7960cfb3``).
3. Strip the dashes to get the hex string
(``d7d20eb8b3c8420c9103401e7960cfb3``).
4. Add the following JSON payload to write the certificate to the device's
filesystem:

.. code-block:: json

{
"files": [
{
"path": "/etc/ssl/certs/device-cert.pem",
"mode": "0600",
"contents": "{{ cert_d7d20eb8b3c8420c9103401e7960cfb3_pem }}"
},
{
"path": "/etc/ssl/private/device-key.pem",
"mode": "0600",
"contents": "{{ cert_d7d20eb8b3c8420c9103401e7960cfb3_key }}"
}
]
}

When this template is assigned to a device, OpenWISP will automatically
replace the variables with the exact, unique X.509 certificate and private
key generated for that specific device.

.. _certificate_templates_api:

Certificate Templates via the REST API

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check if the rest api docs page needs to be updated.
Let's also double check that both the swagger API docs (/api/v1/docs/) and the DRF browsable API reflect the new fields properly.

--------------------------------------

The Certificate Template architecture is fully supported by the
:doc:`OpenWISP REST API </controller/user/rest-api>`:

- The ``type`` field accepts the ``cert`` enumeration.
- ``ca`` and ``blueprint_cert`` are writable fields on the
``/api/v1/controller/template/`` endpoint.
- The ``blueprint_cert`` field is strictly filtered. Attempting to pass
the UUID of an already-assigned or revoked certificate will return a
``400 Bad Request``.
- The Active Mutation Lock is enforced at the serializer level: attempting
to patch the ``type``, ``ca``, or ``blueprint_cert`` of an actively
deployed template will return a ``400 Bad Request``.

Additionally, you can trigger the automated creation and revocation
lifecycle by patching the ``config.templates`` array on the :ref:`Device
endpoint <rest_device_patch>`.
2 changes: 2 additions & 0 deletions docs/user/intro.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ following features:
- **VPN management**: automatically provision VPN tunnel configurations,
including cryptographic keys and IP addresses, e.g.: :doc:`OpenVPN
</user/vpn>`, :doc:`WireGuard <wireguard>`
- :doc:`Certificate Templates <certificate-templates>`: automatically
generate and manage X.509 client certificates for devices
- :doc:`whois`: display information about the public IP address used by
devices to communicate with OpenWISP
- :doc:`import-export`
Expand Down
2 changes: 1 addition & 1 deletion docs/user/rest-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1112,7 +1112,7 @@ You can filter a list of templates based on their backend using the
GET /api/v1/controller/template/?backend={backend}

You can filter a list of templates based on their type using the ``type``
(e.g. vpn or generic).
(e.g. vpn, cert or generic).

.. code-block:: text

Expand Down
6 changes: 3 additions & 3 deletions docs/user/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -308,9 +308,9 @@ levels of OpenWISP, see `netjsonconfig context: configuration variables
The default value of the ``auto_cert`` field for new ``Template`` objects.

The ``auto_cert`` field is valid only for templates which have ``type``
set to ``VPN`` and indicates whether configuration regarding the VPN
tunnel is provisioned automatically to each device using the template,
e.g.:
set to ``VPN`` or ``cert`` and indicates whether configuration regarding
the VPN tunnel (or the x509 certificate) is provisioned automatically to
each device using the template, e.g.:

- when using OpenVPN, new `x509 <https://tools.ietf.org/html/rfc5280>`_
certificates will be generated automatically using the same CA assigned
Expand Down
18 changes: 18 additions & 0 deletions docs/user/templates.rst
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,21 @@ engine: netjsonconfig.
For more advanced technical information about templates, consult the
netjsonconfig documentation: `Basic Concepts, Template
<https://netjsonconfig.openwisp.org/en/latest/general/basics.html#template>`_.

.. _certificate_templates:

Certificate Templates
---------------------

A Certificate Template is a :doc:`Template </controller/user/templates>`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new section mentions certificate templates, but it does not link to the detailed certificate-templates.rst page, and that new page is not added to the user docs toctree in docs/index.rst, so it is not reachable from the built docs. I would add the new page to the toctree and link to it from here.

whose **Type** is set to :guilabel:`Certificate` (``cert``). See
:doc:`/controller/user/certificate-templates` for detailed documentation.

It allows declaring the *Certificate Authority* and an optional *Blueprint
Certificate* that will be used to issue an X.509 certificate for each
device the template is assigned to, without needing a VPN backend.

Certificate Templates are useful for any use case in which a fleet of
devices needs an X.509 client certificate managed centrally by OpenWISP,
for example: mutual TLS authentication against an internal service, signed
device identities for captive portals, etc.
15 changes: 12 additions & 3 deletions openwisp_controller/config/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,12 @@ class Media:
css = {"all": (f"{prefix}css/admin.css",)}
js = list(CopyableFieldsAdmin.Media.js) + [
f"{prefix}js/{file_}"
for file_ in ("preview.js", "unsaved_changes.js", "switcher.js")
for file_ in (
"preview.js",
"unsaved_changes.js",
"switcher.js",
"template_ui.js",
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
]

def get_extra_context(self, pk=None):
Expand Down Expand Up @@ -1060,6 +1065,8 @@ class TemplateAdmin(MultitenantAdminMixin, BaseConfigAdmin, SystemDefinedVariabl
"organization",
"type",
"backend",
"ca",
"blueprint_cert",
"default",
"required",
"created",
Expand All @@ -1074,13 +1081,15 @@ class TemplateAdmin(MultitenantAdminMixin, BaseConfigAdmin, SystemDefinedVariabl
"created",
]
search_fields = ["name"]
multitenant_shared_relations = ("vpn",)
multitenant_shared_relations = ("vpn", "ca", "blueprint_cert")
fields = [
"name",
"organization",
"type",
"backend",
"vpn",
"ca",
"blueprint_cert",
"auto_cert",
"tags",
"default",
Expand All @@ -1092,7 +1101,7 @@ class TemplateAdmin(MultitenantAdminMixin, BaseConfigAdmin, SystemDefinedVariabl
"modified",
]
readonly_fields = ["system_context"]
autocomplete_fields = ["vpn"]
autocomplete_fields = ["vpn", "ca", "blueprint_cert"]

@admin.action(permissions=["add"])
def clone_selected_templates(self, request, queryset):
Expand Down
Loading
Loading