Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Sovereign, high-performance data protection

Database tables

This page describes the tables used by the KMS server to persist its data, and the links between them. It applies to the SQL backends: SQLite, PostgreSQL, MySQL, MariaDB, and Percona XtraDB Cluster.

The Redis-with-Findex backend does not use relational tables; see Redis with Findex.

Overview

The KMS object-store schema is small and consists of six tables. The optional PostgreSQL audit backend adds two more, kms_audit_events and kms_audit_control, in its own, separate database.

TablePurpose
parametersInternal key/value store (migration state, database version, one-time markers)
objectsKMIP objects (keys, certificates, secrets, and so on)
read_accessPer-user read permissions granted on objects
tagsTags attached to objects, used by Locate
crypto_officer_activationsRecords of the Crypto Officer activation ceremony
crlsMost recently signed CRL per issuer CA (RFC 5280 §5), for CDP serving after restart

The links between tables are logical relationships (enforced by the application, not by SQL foreign-key constraints).

erDiagram
    OBJECTS ||--o{ READ_ACCESS : "grants (read_access.id = objects.id)"
    OBJECTS ||--o{ TAGS : "tagged (tags.id = objects.id)"
    OBJECTS ||--o{ OBJECTS : "wraps (objects.wrapping_key_id = objects.id)"
    OBJECTS ||--o| CRLS : "signs (crls.issuer_id = objects.id)"
    PARAMETERS {
        string name PK
        string value
    }
    OBJECTS {
        string id PK
        string object
        string attributes
        string state
        string owner
        string wrapping_key_id FK
    }
    READ_ACCESS {
        string id FK
        string userid
        string permissions
    }
    TAGS {
        string id FK
        string tag
    }
    CRLS {
        string issuer_id PK
        bytes  crl_der
        int    crl_number
        string generated_at
        string next_update
    }
    CRYPTO_OFFICER_ACTIVATIONS {
        timestamp activated_at
        text sealed_record
        timestamp revoked_at
        varchar revoked_by
    }

objects

The central table. One row per KMIP object.

ColumnTypeDescription
idVARCHAR(128)Primary key. The object's unique identifier (UID).
objectVARCHAR (PG/SQLite) / LONGTEXT (MySQL)The serialized KMIP object (JSON).
attributesjsonb (PG) / json (MySQL)The KMIP attributes attached to the object.
stateVARCHAR(32)The KMIP lifecycle state of the object (for example Active, Destroyed).
ownerVARCHAR(255)The user identifier of the object's owner.
wrapping_key_idVARCHAR(128)The UID of the key that wraps this object. Self-reference to objects.id.

The following secondary indexes are created on objects:

IndexColumnsBackends
idx_objects_ownerownerall
idx_objects_statestateall
idx_objects_wrapping_key_idwrapping_key_idall
idx_objects_rotate_nameRotateName attribute, owner (partial: RotateName present)PostgreSQL, SQLite
idx_objects_rotate_autostate (partial: RotateAutomatic = true)PostgreSQL, SQLite
idx_objects_type_stateObjectType attribute, statePostgreSQL, SQLite

Indexes are created at server startup. On an existing large database, the first start after an upgrade takes proportionally longer, and PostgreSQL blocks writes to objects while each new index builds.

read_access

Stores the operations that a given user is allowed to perform on a given object.

ColumnTypeDescription
idVARCHAR(128)The object UID. References objects.id.
useridVARCHAR(255)The user identifier granted access.
permissionsjsonThe operations granted to the user, serialized as JSON.

The pair (id, userid) is unique. In PostgreSQL and SQLite it is declared UNIQUE (id, userid); in MySQL (since 5.13.0) it is the composite PRIMARY KEY (id, userid).

A secondary index idx_read_access_userid is created on userid.

tags

Stores the tags attached to objects. Tags are used to locate objects by tag.

ColumnTypeDescription
idVARCHAR(128)The object UID. References objects.id.
tagVARCHAR(255)A single tag.

The pair (id, tag) is unique. In PostgreSQL and SQLite it is declared UNIQUE (id, tag); in MySQL (since 5.13.0) it is the composite PRIMARY KEY (id, tag).

A secondary index idx_tags_tag_id on (tag, id) supports Locate-by-tags lookups.

parameters

A generic key/value store used internally by the KMS for database metadata.

ColumnTypeDescription
nameVARCHAR(128)Primary key. The parameter name.
valueVARCHAR(256)The parameter value.

Known parameters:

nameMeaning
db_stateThe database migration state: ready or upgrading.
db_versionThe version of the KMS software that last ran against this database.
wrapping_key_id_backfilledA one-time marker recording that the objects.wrapping_key_id backfill has completed.
certificate_serial_tag_backfilledA one-time marker recording that the _serial=<HEX> tag backfill for pre-existing certificates has completed.

crypto_officer_activations

Records the Crypto Officer activation ceremony. One row is added each time the Crypto Officer role is activated via a split-key ceremony.

ColumnTypeDescription
activated_atTIMESTAMPWhen the activation was created (defaults to CURRENT_TIMESTAMP).
sealed_recordTEXTThe sealed record produced by the activation ceremony (AES-256-GCM encrypted).
revoked_atTIMESTAMPWhen the activation was revoked, or NULL while still active.
revoked_byVARCHAR(255)The user who revoked the activation.

In MySQL, an additional id INTEGER PRIMARY KEY AUTO_INCREMENT column is added. In PostgreSQL and SQLite there is no explicit id column; the active activation is the latest row where revoked_at IS NULL.

crls

Stores the most recently generated CRL for each issuer CA, persisted so that the public CDP endpoint (GET /public/certificates/{issuer_id}/crl) can serve the last signed CRL immediately after a server restart without requiring a manual generate-crl call.

One row per CA certificate. The row is replaced atomically on every CRL regeneration (upsert on issuer_id).

ColumnTypeDescription
issuer_idVARCHAR(128)Primary key. The UID of the issuer CA certificate in the objects table.
crl_derBYTEA (PG) / BLOB (SQLite) / LONGBLOB (MySQL)DER-encoded signed CRL bytes.
crl_numberBIGINTMonotonically increasing CRL sequence number (RFC 5280 §5.2.3).
generated_atVARCHAR(32)ISO-8601 UTC timestamp of when this CRL was signed.
next_updateVARCHAR(32)ISO-8601 UTC timestamp of CRL expiry (= generated_at + validity days).

The Redis-with-Findex backend stores each CRL as a JSON value under the key crl:<issuer_id>.

  • objects.id is referenced by read_access.id and tags.id: one object can have many access rows and many tags.
  • objects.wrapping_key_id points to objects.id: a wrapping key is itself an object, and many objects can be wrapped by the same key.
  • crls.issuer_id logically references objects.id (the CA certificate): one CA has at most one current CRL row.
  • objects.owner and read_access.userid hold user identifiers. Users are authenticated identities and are not stored in a dedicated table.
  • parameters and crypto_officer_activations are standalone and do not reference objects.

These relationships are managed by the application layer (crate/server_database/) rather than database foreign-key constraints.

PostgreSQL audit database

Used only by the PostgreSQL audit backend (--audit-postgres-url), in a database distinct from the object store. Its schema is defined in audit.sql.

kms_audit_events

Append-only, hash-chained audit events. One row per event.

ColumnTypeDescription
instance_idTEXTKMS instance owning the chain (1 to 255 characters).
chain_generationBIGINTChain generation (≥ 0); a new one starts after a seal-and-roll.
idBIGINTPosition of the event in its generation (≥ 0).
timestampTIMESTAMPTZWhen the event was recorded (microsecond precision).
operationTEXTKMIP operation or audit:* system event.
usernameTEXTAuthenticated user.
object_uidTEXTUID of the object concerned, or NULL.
algorithmTEXTAlgorithm used, or NULL.
client_ipTEXTClient IP address, or NULL.
resultTEXTOutcome of the operation.
duration_msBIGINTDuration of the operation in milliseconds (≥ 0).
request_idUUIDRequest correlation ID, or NULL.
detailsTEXTEvent-specific JSON details, or NULL.
prev_hashBYTEASHA-256 row_hash of the previous event (32 bytes; zeros for the first event).
row_hashBYTEASHA-256 of this row's fields and prev_hash (32 bytes).

The primary key is (instance_id, chain_generation, id). A secondary index idx_kms_audit_events_timestamp on timestamp serves auditors' time-range queries; the KMS does not use it.

These triggers keep the table append-only:

TriggerFiresEffect
kms_audit_no_updateBEFORE UPDATE, per rowRejects the update (kms_audit_reject_mutation()).
kms_audit_no_deleteBEFORE DELETE, per rowRejects the delete (kms_audit_reject_mutation()).
kms_audit_no_truncateBEFORE TRUNCATE, per statementRejects the truncate (kms_audit_reject_mutation()).
kms_audit_no_insert_sealedBEFORE INSERT, per rowWhen the instance has a kms_audit_control row, rejects an insert into any generation other than its active_generation (kms_audit_reject_sealed_insert()).

UPDATE, DELETE and TRUNCATE are also revoked from PUBLIC.

kms_audit_control

The generation currently accepting writes, per instance.

ColumnTypeDescription
instance_idTEXTPrimary key. KMS instance (1 to 255 characters).
active_generationBIGINTGeneration currently accepting writes (≥ 0).
updated_atTIMESTAMPTZWhen active_generation last changed (defaults to now()).

kms_audit_events.instance_id logically references kms_audit_control.instance_id; as for the object-store tables, there is no foreign-key constraint.