Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
221725e
FUND-2062 Added Encryption for ZRC BSN
ragaumon Mar 31, 2026
b4fbebe
FUND-2062 Removed unneeded Hashing
ragaumon Apr 1, 2026
74ee9e3
FUND-2062 Created generic Hasher
ragaumon Apr 2, 2026
8b789cd
FUND-2062 Made generic DataProtection
ragaumon Apr 2, 2026
d82ad7a
FUND-2062 Added MIgration for encryption
ragaumon Apr 2, 2026
3d9563d
FUND-2062 Some clean up
ragaumon Apr 7, 2026
a040f0a
FUND-2062 Updated README file
ragaumon Apr 7, 2026
c32d64c
FUND-2062 Fixes EF MIgrations
ragaumon Apr 7, 2026
6055df8
FUND-2062 Updated README.md
ragaumon Apr 8, 2026
bb8b7a6
FUND-2062 Added separated Connection for `DataProtectionKeyDbContext`
ragaumon Apr 8, 2026
3e9893b
FUND-2062 Moved to separate project DataProtectionKeyDbContext. Moved…
ragaumon Apr 8, 2026
4c4c774
FUND-2062 Added validation
ragaumon Apr 8, 2026
5e625df
FUND-2062 Updated InpBsnBackfillService
ragaumon Apr 9, 2026
4d4b1ec
FUND-2062 Updated InpBsnBackfillService and other comment fix
ragaumon Apr 9, 2026
51a70d5
Merge remote-tracking branch 'origin/main' into feature/FUND-2062_zrc…
ragaumon Apr 9, 2026
9c3c699
Updated Readme files
ragaumon Apr 10, 2026
2facfab
Formating md
ragaumon Apr 13, 2026
93e7f4b
Formating md
ragaumon Apr 13, 2026
f2e3ec2
Formating md
ragaumon Apr 13, 2026
7805a37
Formating md
ragaumon Apr 13, 2026
79b2f0a
Fixed BackfillService
ragaumon Apr 13, 2026
3ecc802
Updated md files
ragaumon Apr 13, 2026
a1e9d27
Updated md files
ragaumon Apr 13, 2026
9f03fe3
Merge remote-tracking branch 'origin/main' into feature/FUND-2062_zrc…
ragaumon Apr 14, 2026
b740b39
Reverted Zaken README.md
ragaumon Apr 14, 2026
57b38b9
Fixed InpBsnBackfillService
ragaumon Apr 15, 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 .github/workflows/cd-zaken.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ on:
- 'src/OneGround.ZGW.Common.Abstractions/**'
- 'src/OneGround.ZGW.Common.Contracts/**'
- 'src/OneGround.ZGW.Common.DataModel/**'
- 'src/OneGround.ZGW.Common.DataProtection.DataModel/**'
- 'src/OneGround.ZGW.Common.Messaging/**'
- 'src/OneGround.ZGW.Common.ServiceAgent/**'
- 'src/OneGround.ZGW.Common.Web/**'
Expand Down
1 change: 1 addition & 0 deletions getting-started/docker-compose/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ services:
ASPNETCORE_URLS: "http://*:${ASPNETCORE_INTERNAL_PORT}"
ConnectionStrings__UserConnectionString: "Host=${POSTGRES_HOST};Port=${POSTGRES_PORT};Database=${POSTGRES_ZRC_DB};Username=${POSTGRES_USER};Password=${POSTGRES_USER_PASSWORD}"
ConnectionStrings__AdminConnectionString: "Host=${POSTGRES_HOST};Port=${POSTGRES_PORT};Database=${POSTGRES_ZRC_DB};Username=${POSTGRES_ADMIN};Password=${POSTGRES_ADMIN_PASSWORD}"
ConnectionStrings__DataProtectionConnectionString: "Host=${POSTGRES_HOST};Port=${POSTGRES_PORT};Database=${POSTGRES_ZRC_DB};Username=${POSTGRES_ADMIN};Password=${POSTGRES_ADMIN_PASSWORD}"
networks:
- oneground

Expand Down
66 changes: 64 additions & 2 deletions localdev/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
- [Step 5.1: Get the Client Secret from Keycloak](#step-51-get-the-client-secret-from-keycloak)
- [Step 5.2: Update Environment File and Restart Services](#step-52-update-environment-file-and-restart-services)
- [Step 5.3: Request an Access Token](#step-53-request-an-access-token)
- [6. Stopping the Services](#6-stopping-the-services)
- [6. Configure Data Protection & Encryption (Zaken API)](#6-configure-data-protection--encryption-zaken-api)
- [7. Stopping the Services](#7-stopping-the-services)
- [Service Endpoints and Tools](#service-endpoints-and-tools)
- [ZGW API Services/listeners](#zgw-api-serviceslisteners)
- [Hosted Tools](#hosted-tools)
Expand Down Expand Up @@ -195,7 +196,68 @@ See [AUTHENTICATION.md](../docs/AUTHENTICATION.md).

See [AUTHENTICATION.md](../docs/AUTHENTICATION.md).

### 6. Stopping the Services
### 6. Configure Data Protection & Encryption (Zaken API)

The Zaken API supports encryption of sensitive personal data (BSN - Burgerservicenummer) stored in the database. This uses two mechanisms:

#### HMAC Hashing (for searchable lookups)

BSN values are hashed using HMAC-SHA256 so they can be searched without storing plaintext. Configure the HMAC key via an environment variable on the Zaken container in `default.env`:

```text
HmacHasher__HmacKey=<base64-encoded-key-minimum-32-bytes>
```

To generate a key:

```bash
# Linux/macOS
openssl rand -base64 32

# PowerShell
[Convert]::ToBase64String((1..32 | ForEach-Object { Get-Random -Maximum 256 }) -as [byte[]])
```

> **Warning**: The HMAC key is permanent — if you change it, existing hashes become unsearchable. Back it up securely.

#### DataProtection Encryption (for reversible encryption at rest)

BSN values are also encrypted using ASP.NET Core DataProtection. Encryption keys are stored in the database (`data_protection.DataProtectionKeys` table). Optionally, these keys can be protected with an X.509 certificate.

Add the following to `default.env`:

```text
DataProtection__Certificate=<base64-encoded-pfx>
DataProtection__CertificatePassword=<pfx-password>
```

Use the provided generator script to create a certificate. Run the following commands **from the `localdev` directory**. The script outputs the values ready to paste into `default.env`:

**Linux/macOS:**

```bash
chmod +x ../tools/oneground-certificates-generator/generate-dataprotection-certificate.sh
../tools/oneground-certificates-generator/generate-dataprotection-certificate.sh
```

**Windows (PowerShell):**

```powershell
Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process
..\tools\oneground-certificates-generator\generate-dataprotection-certificate.ps1
```

> **Warning**: If the certificate is lost, all encrypted data in the database becomes permanently unreadable. Always back up the PFX file.

If no certificate is configured, DataProtection keys are stored unencrypted in the database. This is acceptable for local development but not recommended for production.

After updating `default.env`, restart the services:

```bash
docker compose --env-file ./.env up -d
```

### 7. Stopping the Services

To stop all running Docker containers, run the following command from the `localdev` directory:

Expand Down
1 change: 1 addition & 0 deletions localdev/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ services:
ASPNETCORE_URLS: "http://*:${ASPNETCORE_INTERNAL_PORT}"
ConnectionStrings__UserConnectionString: "Host=${POSTGRES_HOST};Port=${POSTGRES_PORT};Database=${POSTGRES_ZRC_DB};Username=${POSTGRES_USER};Password=${POSTGRES_USER_PASSWORD}"
ConnectionStrings__AdminConnectionString: "Host=${POSTGRES_HOST};Port=${POSTGRES_PORT};Database=${POSTGRES_ZRC_DB};Username=${POSTGRES_ADMIN};Password=${POSTGRES_ADMIN_PASSWORD}"
ConnectionStrings__DataProtectionConnectionString: "Host=${POSTGRES_HOST};Port=${POSTGRES_PORT};Database=${POSTGRES_ZRC_DB};Username=${POSTGRES_ADMIN};Password=${POSTGRES_ADMIN_PASSWORD}"
networks:
- oneground

Expand Down
30 changes: 30 additions & 0 deletions localdev/postgresql/init-database.sh/init-database.sh
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,30 @@ grant_hangfire_permissions() {
EOSQL
}

grant_protection_permissions() {
local db_name=$1
local admin_role="oneground_admin"
local user_role="oneground_user"

psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$db_name" <<-EOSQL
CREATE SCHEMA IF NOT EXISTS data_protection AUTHORIZATION ${user_role};

GRANT ALL ON SCHEMA data_protection TO ${user_role};
GRANT ALL ON ALL TABLES IN SCHEMA data_protection TO ${user_role};
GRANT ALL ON ALL SEQUENCES IN SCHEMA data_protection TO ${user_role};

GRANT ALL ON SCHEMA data_protection TO ${admin_role};
GRANT ALL ON ALL TABLES IN SCHEMA data_protection TO ${admin_role};
GRANT ALL ON ALL SEQUENCES IN SCHEMA data_protection TO ${admin_role};

ALTER DEFAULT PRIVILEGES FOR ROLE ${admin_role} IN SCHEMA data_protection GRANT ALL ON TABLES TO ${user_role};
ALTER DEFAULT PRIVILEGES FOR ROLE ${admin_role} IN SCHEMA data_protection GRANT ALL ON SEQUENCES TO ${user_role};

ALTER DEFAULT PRIVILEGES FOR ROLE ${admin_role} IN SCHEMA data_protection GRANT ALL ON TABLES TO ${admin_role};
ALTER DEFAULT PRIVILEGES FOR ROLE ${admin_role} IN SCHEMA data_protection GRANT ALL ON SEQUENCES TO ${admin_role};
EOSQL
}

psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE ROLE oneground_admin WITH LOGIN PASSWORD 'oneground_admin' NOSUPERUSER INHERIT NOCREATEDB NOCREATEROLE NOREPLICATION;
CREATE ROLE oneground_user WITH LOGIN PASSWORD 'oneground_user' NOSUPERUSER INHERIT NOCREATEDB NOCREATEROLE NOREPLICATION;
Expand All @@ -79,3 +103,9 @@ DATABASES_WITH_HANGFIRE="drc_db nrc_db"
for db in $DATABASES_WITH_HANGFIRE; do
grant_hangfire_permissions "$db"
done

DATABASES_WITH_PROTECTION="zrc_db"

for db in $DATABASES_WITH_PROTECTION; do
grant_protection_permissions "$db"
done
4 changes: 3 additions & 1 deletion src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
<PackageVersion Include="MassTransit.RabbitMQ" Version="8.4.1" />
<PackageVersion Include="MediatR" Version="12.5.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.25" />
<PackageVersion Include="Microsoft.AspNetCore.DataProtection.Abstractions" Version="8.0.25" />
<PackageVersion Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" Version="8.0.25" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.25" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.25" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.25" />
Expand Down Expand Up @@ -65,4 +67,4 @@
<PackageVersion Include="Swashbuckle.AspNetCore.Filters" Version="8.0.3" />
<PackageVersion Include="Swashbuckle.AspNetCore.Newtonsoft" Version="8.1.4" />
</ItemGroup>
</Project>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace OneGround.ZGW.Common.DataProtection.DataModel;

public static class DataProtectionDbContextExtensions
{
public static IServiceCollection AddDataProtectionDbContext(this IServiceCollection services, IConfiguration configuration)
{
var connectionString = configuration.GetConnectionString("DataProtectionConnectionString");
if (string.IsNullOrWhiteSpace(connectionString))
throw new InvalidOperationException("No valid 'DataProtectionConnectionString' specified in ConnectionStrings.");

services.AddDbContext<DataProtectionKeyDbContext>(dbContextOptionsBuilder =>
{
dbContextOptionsBuilder.UseNpgsql(connectionString, o => o.MigrationsHistoryTable("__EFMigrationsHistory", "data_protection"));
});

return services;
}

/// <summary>
/// Migrates the DataProtection database schema eagerly during startup.
/// Must be called before app.Run() to ensure the schema exists before ASP.NET DataProtection reads keys.
/// </summary>
public static async Task MigrateDataProtectionDatabaseAsync(this WebApplication app)
{
using var scope = app.Services.CreateScope();
await scope.ServiceProvider.GetRequiredService<DataProtectionKeyDbContext>().Database.MigrateAsync();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace OneGround.ZGW.Common.DataProtection.DataModel;

public class DataProtectionKeyDbContext : DbContext, IDataProtectionKeyContext
{
public DataProtectionKeyDbContext(DbContextOptions<DataProtectionKeyDbContext> options)
: base(options) { }

public DbSet<DataProtectionKey> DataProtectionKeys { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("data_protection");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using OneGround.ZGW.DataAccess;

namespace OneGround.ZGW.Common.DataProtection.DataModel;

public class DataProtectionKeyDbContextFactory : BaseDbContextFactory<DataProtectionKeyDbContext>
{
public DataProtectionKeyDbContextFactory(IConfiguration configuration)
: base(configuration, "DataProtectionConnectionString") { }

public DataProtectionKeyDbContextFactory()
: base("DataProtectionConnectionString") { }

public override DataProtectionKeyDbContext CreateDbContext(string[] args)
{
var optionsBuilder = new DbContextOptionsBuilder<DataProtectionKeyDbContext>();
optionsBuilder.UseNpgsql(ConnectionString, o => o.MigrationsHistoryTable("__EFMigrationsHistory", "data_protection"));
return new DataProtectionKeyDbContext(optionsBuilder.Options);
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Comment thread
ragaumon marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;

#nullable disable

namespace OneGround.ZGW.Common.DataProtection.DataModel.Migrations
{
/// <inheritdoc />
public partial class create_data_protection_keys : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(name: "data_protection");

migrationBuilder.CreateTable(
name: "DataProtectionKeys",
schema: "data_protection",
columns: table => new
{
Id = table
.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
FriendlyName = table.Column<string>(type: "text", nullable: true),
Xml = table.Column<string>(type: "text", nullable: true),
},
constraints: table =>
{
table.PrimaryKey("PK_DataProtectionKeys", x => x.Id);
}
);
}

/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(name: "DataProtectionKeys", schema: "data_protection");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using OneGround.ZGW.Common.DataProtection.DataModel;

#nullable disable

namespace OneGround.ZGW.Common.DataProtection.DataModel.Migrations
{
[DbContext(typeof(DataProtectionKeyDbContext))]
partial class DataProtectionKeyDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("data_protection")
.HasAnnotation("ProductVersion", "8.0.25")
.HasAnnotation("Relational:MaxIdentifierLength", 63);

NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);

modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");

NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));

b.Property<string>("FriendlyName")
.HasColumnType("text");

b.Property<string>("Xml")
.HasColumnType("text");

b.HasKey("Id");

b.ToTable("DataProtectionKeys", "data_protection");
});
#pragma warning restore 612, 618
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>OneGround.ZGW.Common.DataProtection.DataModel</AssemblyName>
<RootNamespace>OneGround.ZGW.Common.DataProtection.DataModel</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\OneGround.ZGW.DataAccess\ZGW.DataAccess.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
</ItemGroup>
</Project>
Loading
Loading