March 18, 2026

django-security-label: A third-party package to anonymize data in your models

Django Security Label is a Django third-party package to help you define rules to dynamically anonymize/mask your data for PostgreSQL databases. It makes use of the PostgreSQL Anonymizer extension and supports creating any type of SECURITY LABEL.

This package has not been used in production yet. If you do, please open an issue and let us know how it went for you!

This package was created with Jay Miller, Staff Developer Advocate at Aiven, for the “Elephant in the Room” series. The inspiration for the project was from his blog post, “Using PostgreSQL Anonymizer to safely share data with LLMs.”

The superpower of this package is to allow you to define what fields have sensitive data and the scenarios that they should be masked directly on your models. When these scenarios are hit, the database will return masked data. Your Django application does not need to worry about anonymizing data or leaking information through memory. It only has access to the anonymized data. And it’s all defined on your models.

Let’s look at an example:


from django.db import models
from django_security_label import labels

class Prospect(models.Model):
    """Represents a someone interested in your product or service."""
    name = models.CharField(max_length=255)
    email = models.EmailField()

    class Meta:
        indexes = [
            # Define the anonymization/masking rules as indexes. This ideally
            # will change in the future, but it works!
			labels.MaskColumn(
			    fields=["name"],
			    mask_function=labels.MaskFunction.dummy_name,
			),
			labels.MaskColumn(
			    fields=["email"],
			    mask_function=labels.MaskFunction.dummy_safe_email,
			),
        ]

With the above we are saying there are two scenarios. Needing anonymization and needing raw data. When we need anonymization, the masking rules will generate a fake name and email for every query. When we need the raw data, the query will return the actual data.

With the rules in place (don’t forget to makemigrations and migrate), we need to determine when we are in each of these scenarios and activate those masking rules. The best place for this is in the MIDDLEWARE setting. This package provides two pre-built middlewares:

  • MaskedReadsMiddleware: Only user.is_superuser can view the raw data, all other users will have the masking rules applied
  • GroupMaskingMiddleware: Allow auth.Group to determine which security policy to use to support multiple types of anonymization/masking on the same column.

Because the scenarios in which data anonymization is needed are so varied and specific to business rules, we don’t have many pre-built solutions. Instead you should review their code to see how you can implement what is necessary for your application.

As an engineer we try to “shift-left” on security. This is a piece of that. As we create the data model, there’s an understanding of what data is going to be sensitive. We also know who should have access to what and what facets of data are risky noise. Defining the masking rules on models helps us consider the implications of what is actually being stored in the models and how to manage that risk.

How to use this

Beyond the masked and unmasked scenarios that MaskedReadsMiddleware provides, you could use this application to limit the data that specific types of users can access. You could also set up the masking rules to only be applied in a staging or reporting environment by swapping out middlewares or building your own middleware.

For now though, let’s focus on the former. Your application likely has different users with different roles accessing it. The people in those roles likely don’t need access to everything. With django-security-label, you can limit their access at the database level.

Django auth.Group integration example

Let’s consider we’re in the shipping / logistics business. We have a table of drivers with some personal information.

from django.db import models
from django_security_label import labels

class Driver(models.Model):
    """
    Represents a delivery driver in the fleet.

    Mask sensitive contact and vehicle details from dispatchers,
    who only require availability and zone information to assign deliveries.
    """
    class Status(models.TextChoices):
        AVAILABLE = "available", "Available"
        ON_DELIVERY = "on_delivery", "On Delivery"
        OFFLINE = "offline", "Offline"

    full_name = models.CharField(max_length=255)
    email = models.EmailField()
    phone = models.CharField(max_length=20)
    license_number = models.CharField(max_length=50)
    vehicle_plate = models.CharField(max_length=20)
    current_zone = models.CharField(max_length=100)
    status = models.CharField(
        max_length=20,
        choices=Status.choices,
        default=Status.OFFLINE,
    )

    class Meta:
        indexes = [
			# Masking rules for dispatchers
			labels.AnonymizeColumn(
				fields=["email"],
				policy="dispatcher",
				string_literal="MASKED WITH FUNCTION anon.partial_email(email)",
				name="driver_email_dispatcher",
			),
			labels.AnonymizeColumn(
				fields=["phone"],
				policy="dispatcher",
				string_literal="MASKED WITH VALUE $$hidden$$",
				name="driver_phone_dispatcher",
			),
			labels.AnonymizeColumn(
				fields=["license_number"],
				policy="dispatcher",
				string_literal="MASKED WITH VALUE $$hidden$$",
				name="driver_license_number_dispatcher",
			),
			labels.AnonymizeColumn(
				fields=["vehicle_plate"],
				policy="dispatcher",
				string_literal="MASKED WITH FUNCTION anon.partial_email(vehicle_plate, 0, $$***$$, 3)",
				name="driver_vehicle_plate_dispatcher",
			),
			# Masking rules for developers
			labels.MaskColumn(
				fields=["email"],
				policy="dev",
				mask_function=labels.MaskFunction.dummy_safe_email,
				name="driver_email_dev",
			),
			labels.MaskColumn(
				fields=["phone"],
				policy="dev",
				mask_function=labels.MaskFunction.dummy_phone_number,
				name="driver_phone_dev",
			),
			labels.AnonymizeColumn(
				fields=["license_number"],
				policy="dev",
				string_literal="MASKED WITH VALUE $$hidden$$",
				name="driver_license_number_dev",
			),
			labels.MaskColumn(
				fields=["vehicle_plate"],
				policy="dev",
				mask_function=labels.MaskFunction.dummy_licence_plate,
				name="driver_vehicle_plate_dev",
			),
        ]

With the above we have three scenarios to account for.

  1. Dispatchers accessing the data, who may need some partial information to answer questions, but don’t need all the data
  2. Developers accessing the data, who don’t need contact information, but should have properly shaped data
  3. Everyone else, who can read all data

The above can be implemented using GroupMaskingMiddleware and the following setting.

SECURITY_LABEL_GROUPS_TO_POLICIES = [
    ("Dispatchers", "dispatcher"),
    ("Developers", "dev"),
    ("Compliance", None),
]

This setting, combined with GroupMaskingMiddleware will have users who are in the specified group, have the specified masking policy used. This is what connects the actual users of the web application to the specific masking rules that have been configured in Driver.Meta.indexes.

Creating a custom middleware

For example, you could implement a middleware that only utilizes the masking rules when the request is from an unauthenticated user.

from __future__ import annotations

from django.db import connection
from django.http import HttpRequest
from django_security_label.middleware import enable_masked_reads, disable_masked_reads

def use_masked_reads(request: HttpRequest) -> bool:
    user = getattr(request, "user", None)
    return user is None or not user.is_authenticated

class AnonymousOnlyMaskedReadsMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
    def __call__(self, request):
	    if enable_masking := use_masked_reads(request):
		    enable_masked_reads()

        response = self.get_response(request)

        if enable_masking:
	        disable_masked_reads()
        return response

At the core of this package, we are changing the role (PostgreSQL’s concept of a user) of the database connection temporarily via SET SESSION ROLE <something>;.

This package automatically creates a new role, that when activated, will have all masking rules applied for it. This role can be activated by calling the function django_security_label.middleware.enable_masked_reads(). After you fetch the data needed, you need to change the role back on the connection with disable_masked_reads().

What’s next

This package is far from feature complete, but it is in need of feedback and guidance.

Given the application of this package is so business specific, I’d love to hear from you on how it could be used or extended. If you do try it out, please let me know how it went. You can create an issue or reach out to me personally.


If you have thoughts, comments or questions, please let me know. You can find me on the Fediverse, Django Discord server or via email.