DSS Signing Server¶
The {{ product_name }} DSS signing server (alpine-dss-server) is an eIDAS remote-signing
service. It performs hash-only document signing: callers send a digest (never the document), and
the server signs it with a private key held in an HSM partition pool through the {{ product_name }}
HSM abstraction (alpine-hsm / alpine-pools). Signing always runs on the tenant's Securosys
Primus partition: the partition type is fixed in the code that resolves it, so there is no
software-key signing path and no setting that could enable one.
It is a single Spring Boot process exposing a JSON/REST API. In the signing chain it is the central, shared service that the on-prem DSS Proxy calls with only document hashes.
How the document stays private
The server signs a digest, not a document. The document bytes never leave the caller (for the proxy, they never leave the tenant network). This is what makes the proxy → server split privacy-preserving.
Endpoints¶
| Method & path | Purpose | Authentication |
|---|---|---|
POST /api/server-signing/sign-ska |
Sign with a tenant's Securosys Primus SKA key, tenant/module-scoped | Self-authenticating DSS-proxy JWT |
GET / |
Branded service index (no signing surface) | Public |
GET /actuator/health |
Liveness/health probe | Public |
/sign-ska is the whole signing surface. Signing is always SKA-authorized: the private key stays in
the tenant's Securosys Primus partition and every operation carries a hardware-enforced authorization
from the key profile's authorization key.
Deployment¶
Prerequisites¶
- Java 25 only.
- A Securosys Primus HSM partition reachable through the {{ product_name }} HSM abstraction. Its host, user and PIN are read from the control database per request — never from this service's configuration.
- PostgreSQL 18 control database — required to sign:
/sign-skaresolves the caller's registered public key, licence and permission from it. The tenant must have a provisioned HSM partition (provision hsm), thedssfeature license, and thedss.signpermission. - Optionally a JWT key source — a JWKS URI, a public-key file, or an OIDC issuer — for the default-deny filter in front of every path that is not public and does not authenticate itself. Signing does not need one.
Build and run¶
# Build this module and its dependencies
mvn -pl 07_dss/alpine-dss-server -am package
# A control database is still required to sign: /sign-ska reads the caller's key,
# licence and permission from it.
java -jar 07_dss/alpine-dss-server/target/alpine-dss-server-<version>.jar \
--alpine.dss.control-db.url=jdbc:postgresql://localhost:5432/alpine_control \
--alpine.dss.control-db.username=dss_ro \
--alpine.dss.control-db.password="$ALPINE_DSS_CONTROL_DB_PASSWORD"
java -jar alpine-dss-server-<version>.jar \
--alpine.dss.control-db.url=jdbc:postgresql://db-host:5432/alpine_control \
--alpine.dss.control-db.username=dss_ro \
--alpine.dss.control-db.password="$ALPINE_DSS_CONTROL_DB_PASSWORD"
Add a JWT key source (--spring.security.oauth2.resourceserver.jwt.jwk-set-uri=…) only if something
other than /sign-ska is to be reachable; signing itself does not consult it.
The server listens on port 8080 by default (--server.port=<port> to change it). There is no
bootstrap step: the server is ready as soon as a partition and the control database are configured. If
/sign-ska is called without a control database, it returns 503 Service Unavailable.
Configuration¶
Configuration is in 07_dss/alpine-dss-server/src/main/resources/application.yml and can be overridden
with command-line arguments or environment variables (Spring relaxed binding).
| Key | Env override | Default | Description |
|---|---|---|---|
server.port |
SERVER_PORT |
8080 |
HTTP listening port. |
alpine.dss.security.permit-all |
ALPINE_DSS_SECURITY_PERMIT_ALL |
false |
Dev only — permit every request at the resource-server layer. /sign-ska still verifies its own DSS JWT. |
alpine.dss.security.max-request-bytes |
ALPINE_DSS_SECURITY_MAX_REQUEST_BYTES |
16777216 (16 MiB) |
Largest request body accepted under /api/**. An over-sized Content-Length is refused with 413; a chunked body is aborted once it streams past the cap. Zero or less falls back to the 16 MiB default. |
alpine.dss.signing.timeout-seconds |
ALPINE_DSS_SIGNING_TIMEOUT_SECONDS |
30 |
Max wait for a signing operation on the partition-pool worker. |
alpine.dss.control-db.url |
ALPINE_DSS_CONTROL_DB_URL |
(unset) | JDBC URL of the read-only control DB. Set enables SKA signing; unset → /sign-ska returns 503. |
alpine.dss.control-db.username |
ALPINE_DSS_CONTROL_DB_USERNAME |
(unset) | Control-DB user (a read-only role is recommended). |
alpine.dss.control-db.password |
ALPINE_DSS_CONTROL_DB_PASSWORD |
(unset) | Control-DB password (supply via env / secrets manager). |
alpine.dss.control-db.schema |
ALPINE_DSS_CONTROL_DB_SCHEMA |
(unset) | Optional schema; may be blank. |
alpine.dss.control-db.maximum-pool-size |
ALPINE_DSS_CONTROL_DB_MAXIMUM_POOL_SIZE |
4 |
HikariCP maximum pool size. |
spring.security.oauth2.resourceserver.jwt.jwk-set-uri |
(relaxed binding) | (unset) | JWKS endpoint (RSA/EC, rotatable via kid). |
spring.security.oauth2.resourceserver.jwt.public-key-location |
(relaxed binding) | (unset) | Single RSA public-key file (classpath: or path). |
spring.security.oauth2.resourceserver.jwt.issuer-uri |
(relaxed binding) | (unset) | OIDC issuer (auto-discovers the JWKS URI). |
Table: the DSS signing server's operator-facing configuration keys. There is no partition setting here —
the partition is resolved per caller from the control database. Supply a JWT key source only if a path
other than /sign-ska has to be reachable.
permit-all is a development escape hatch
alpine.dss.security.permit-all=true makes every path unauthenticated at the resource-server
layer and logs a prominent warning at startup. Never enable it in production. It does not weaken
/sign-ska, which verifies the dss-proxy's DSS JWT itself.
HSM PINs are never in this configuration
The server does not hold HSM connection secrets. Partition host, PIN and credentials are managed in
the HSM partition registry (see provision hsm) and resolved by the
alpine-hsm driver at runtime — never printed in this service's config.
Authentication¶
The server has two authentication layers: the signing endpoint authenticates itself, and a resource-server filter denies everything that is neither public nor self-authenticating.
/sign-ska self-authenticates the dss-proxy's client-signed DSS JWT via DssProxyAuthenticator
instead of the resource-server filter. It verifies the token's issuer, audience, kid and tenant
claim, then looks up the caller's public key scoped to the tenant's DSS module instance
((tenantId, kid)) from the control DB — a key registered for a different module/tenant is rejected.
The request's in-band tenantId and moduleUuid must match the authenticated identity (fail-closed).
The signing call is then gated by the dss feature license (@LicenseRequireFeature) and the
dss.sign permission (@RequiresAuthorization), both resolved from the control DB.
The index page, its static assets, the styled error view and GET /actuator/health are public;
/sign-ska is permitted at this layer because it verifies its own token. Every other path requires a
bearer token, which Spring Security's OAuth2 resource server validates as a client-signed JWT
(RSA or EC) against the configured key source (jwk-set-uri / public-key-location / issuer-uri),
time claims (exp, nbf, iat) included. With no key source configured and permit-all=false, those
paths are denied outright and the server logs a warning at startup — which costs nothing today, because
no endpoint of this service sits behind that filter.
Error responses¶
| HTTP status | When |
|---|---|
401 Unauthorized |
Missing/malformed Authorization header; invalid signature; expired/future token; bad claims. |
403 Forbidden |
Tenant/module mismatch; dss feature not licensed; dss.sign permission absent. |
400 Bad Request |
Invalid request body; a configuration error raised while resolving the signing context. |
413 Content Too Large |
A request under /api/** whose declared Content-Length exceeds alpine.dss.security.max-request-bytes. A body with no declared length is not rejected up-front: it is aborted mid-read once it passes the cap. |
502 Bad Gateway |
Control-DB or HSM error. |
503 Service Unavailable |
/sign-ska called but no control-DB connection is configured. |
FAQ¶
Does the signing server ever see the document?
No. It signs a digest only. The caller (for on-prem use, the DSS Proxy) hashes the document locally and sends just the hash, so the document bytes never reach the server.
Do I need a database to run the signing server?
Yes, to sign. /sign-ska is the only signing endpoint and it resolves the caller's key, licence and
permission from the control DB. When alpine.dss.control-db.url is unset the server still starts and
serves the index and the health probe, and /sign-ska returns 503 Service Unavailable.
What key source should I use for authentication?
Supply exactly one of spring.security.oauth2.resourceserver.jwt.jwk-set-uri (JWKS, supports key
rotation), …public-key-location (a single RSA key), or …issuer-uri (OIDC discovery). It is the key
source for the default-deny filter, not for /sign-ska, which verifies the dss-proxy's DSS JWT against a
key held in the control DB. A deployment that only signs therefore needs no key source at all.
How is qualified (eIDAS) signing enforced?
By construction rather than by configuration. /sign-ska resolves a Securosys Primus partition and signs
with an SKA-protected key held in it, so there is no software-key path to switch off. The partition is
registered and reachable through the HSM registry — see provision hsm.