The API lives on the main port (app.port) under the /api/v1/ prefix. The handlers
are the same controllers that serve the UI, but with separate "API" functions returning
JSON.
The application has three independent mechanisms, each in its own middleware:
| Mechanism | Middleware | Where it applies |
|---|---|---|
| Sessions | AuthMiddleware |
Web interface |
| API key | APIKeyAuth(scope) |
/api/v1/* |
| Basic / mTLS | ESTBasicAuth, ESTCertAuth |
EST endpoints (a different port) |
AuthMiddleware is attached to the whole main application but skips the session check
for two prefixes - /api/v1/ and /.well-known/est/ - since those are protected by
their own middleware at the route level. It also lets public paths through:
/ /login /overview /cert_info
/api/v1/crl/{subca,rootca,bundleca}/{pem,der}
as well as static assets by extension (.css, .js, .svg, .ico, fonts).
There is also an internal API key - a random value generated at startup
(crypts.GetInternalAPIKey). It only exists in memory and is used by background tasks
to call the application's own endpoints.
The database holds not the key itself but HMAC-SHA256(master key, key) in hex. The
original value cannot be recovered from the database - it is shown exactly once, at
creation time.
h := hmac.New(sha256.New, secret) // secret = crypts.AesSecretKey.KeyHMAC rather than a plain hash was chosen because security is then tied to the master key: even with a copy of the database an attacker cannot brute-force the key offline.
APIKeyAuth(requiredScope) performs:
- Key extraction - from the
X-API-Keyheader orAuthorization: Bearer <key>. - HMAC computation and a lookup in
api_keysbykey_hash(a single database query, with no in-memory cache in between). - A check of
key_status(0active,1expired) and theexpires_atdeadline. - A scope check: the
scopesstring is stored comma-separated and must contain the required value. An emptyscopesmeans no permissions. - An update of
last_usedand the client IP.
Errors: 401 if the key is unknown, expired or disabled; 403 if the scope is
insufficient.
| Scope | Permissions |
|---|---|
read |
Retrieving lists |
write |
Creating certificates, servers, entities, generating CRLs |
| Method | Path | Response |
|---|---|---|
| GET | /api/v1/crl/subca/pem | /der |
Sub CA CRL |
| GET | /api/v1/crl/rootca/pem | /der |
Root CA CRL |
| GET | /api/v1/crl/bundleca/pem | /der |
Both CRLs one after another |
They are duplicated on the public port (app.crl_port) - that is where certificate CDPs
point. On the main port they are kept for compatibility.
| Method | Path | Scope | Purpose |
|---|---|---|---|
| POST | /api/v1/crl/bundleca/generate |
write |
Force CRL regeneration |
| POST | /api/v1/server/add_server |
write |
Add a server |
| POST | /api/v1/server/add_entity |
write |
Add a server entity |
| POST | /api/v1/server/add_certs |
write |
Issue a server certificate |
| GET | /api/v1/server/entity_server_list |
read |
List of server entities |
| GET | /api/v1/server/ssh_key_list |
read |
List of SSH keys |
| GET | /api/v1/users/entity_list |
read |
List of client entities |
| GET | /api/v1/users/oid_list |
read |
List of OIDs |
| POST | /api/v1/users/add_certs |
write |
Issue a client certificate |
KEY="<the key shown at creation time>"
# List of entities
curl -sk -H "X-API-Key: $KEY" \
https://tlss.lv.local:43000/api/v1/users/entity_list
# The same through Bearer
curl -sk -H "Authorization: Bearer $KEY" \
https://tlss.lv.local:43000/api/v1/server/entity_server_list
# Issuing a client certificate
curl -sk -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{
"EntityId": 1,
"Algorithm": "RSA",
"KeyLength": 4096,
"TTL": 365,
"CommonName": "user@example.com",
"CountryName": "RU",
"StateProvince": "Udmurt Republic",
"LocalityName": "Izhevsk",
"Organization": "LV Inc.",
"OrganizationUnit": "IT",
"Email": "user@example.com",
"Password": "container-password"
}' \
https://tlss.lv.local:43000/api/v1/users/add_certs
# CRL regeneration
curl -sk -X POST -H "X-API-Key: $KEY" \
https://tlss.lv.local:43000/api/v1/crl/bundleca/generateThe API -> API keys section in the UI. Creating a key asks for a name, scope and
TTL; the key value is shown once. Expired keys are marked by the CheckValidCerts
checker (key_status = 1) and stop working.