Skip to content

tls: add db_* commands for tls_mgm provisioning - #166

Open
dariusstefan wants to merge 7 commits into
masterfrom
tls-db-provisioning
Open

tls: add db_* commands for tls_mgm provisioning#166
dariusstefan wants to merge 7 commits into
masterfrom
tls-db-provisioning

Conversation

@dariusstefan

@dariusstefan dariusstefan commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Motivation

The tls module could only write certificates and keys to files. Provisioning the tls_mgm table — where certificate, private_key and ca_list are stored as BLOBs rather than paths — had to be done by hand in SQL.

What this adds

tls db_add    [domain] [type] column=value ...
tls db_update [domain] [type] column=value ...
tls db_show   [domain] [type]
tls db_delete [domain] [type]
tls db_list

Everything is given as column=value, so the whole schema is reachable — method, cipher_list, verify_cert, match_ip_address and the rest, not just the certificate and the key:

opensips-cli -x tls db_add a.example.org server \
	certificate=/etc/letsencrypt/live/a.example.org/fullchain.pem \
	private_key=/etc/letsencrypt/live/a.example.org/privkey.pem \
	method=TLSv1_2 verify_cert=1

The four columns holding PEM content (certificate, private_key, ca_list, dh_params) take the path of the file holding it; the file is read and its content stored. Every other column is stored as the value it is given, paths included — so ca_list reads its file while ca_dir and crl_dir keep the directory as such, which is what tls_mgm expects of them. The distinction follows the schema: those four are the DB_BLOB columns, the rest are DB_STRING.

db_update changes only the columns it is given, which makes certificate renewal a single command rather than a delete followed by an add.

Columns left unset keep their schema default, so a server domain that relies on the module's global certificate/private_key modparams stays expressible — tls_mgm falls back to them on its own.

Identifying a domain

A row is identified by (domain, type), matching the table's UNIQUE (domain, type) constraint. Both are ordinary columns, so both can be written as column=value:

opensips-cli -x tls db_delete domain=a.example.org type=server

The first two bare arguments are a shorthand for the same two columns, in that order, which keeps the common case short:

opensips-cli -x tls db_delete a.example.org server

The two spellings can be mixed freely; supplying the same column both ways is the one ambiguous case and is refused, as is a third bare argument. id is refused outright — the database generates it.

Whatever is left out is then asked for, in one place rather than one per spelling. db_add and db_show default the type to server; db_update and db_delete have no default and keep asking until one is given, so the two commands that change an existing row cannot pick a different one than intended. db_list addresses no domain and asks for nothing.

There is deliberately no configuration setting for the type: it identifies a row rather than describing the environment, so it does not belong in opensips-cli.cfg next to the database URL.

Notes

  • column=value follows the convention already used by the mi module, by -o/--option and by the interactive set command, and is split with split('=', 1) like all of them.
  • One parser resolves both the identity and the columns, so db_show and db_delete now report the columns they do not take instead of ignoring them.
  • Unknown column names, unreadable files and files that do not hold PEM are rejected before the database is touched, and before anything is asked for.
  • After every change the tls_reload MI command is issued, so a running OpenSIPS picks up the domains without a restart. If OpenSIPS cannot be reached, a warning is logged and the domains load at the next restart.
  • __complete__ offers the column names in interactive mode.
  • The certificate generation path (rootCA, userCERT) is untouched.

Testing

Exercised by hand against MySQL 8.0 and a running OpenSIPS 4.0 with tls_mgm in DB mode: provisioning a certificate and key from PEM files in both the named and the positional form, the scalar columns round-tripping, unset columns coming back as schema NULLs, db_show rendering every column with the private key hidden, rejection of a duplicate (domain, type), of a column given both ways, of a surplus argument, of id= and of an unknown column, db_update patching a single column, and db_delete prompting for the type and removing the row. The tls_reload MI command reached OpenSIPS over the FIFO in each case.

No automated tests are included.

The tls module could only write certificates to files, leaving the
tls_mgm table to be provisioned by hand.  Add CRUD commands over it:

    tls db_add    <domain> [type] column=value ...
    tls db_update <domain> [type] column=value ...
    tls db_show   <domain> [type]
    tls db_delete <domain> [type]
    tls db_list

A domain is identified by its name and its type (server or client),
matching the UNIQUE (domain, type) constraint of the table.  Every
settable column is passed as 'column=value', so the whole schema is
reachable, not just the certificate and the key.  The columns holding
PEM content take the path of the file holding it, which is read and
stored as a BLOB; every other column is stored as given.

Unknown columns, unreadable files and files that do not hold PEM are
rejected before the database is touched.  After every change the
tls_reload MI command is issued, so a running OpenSIPS picks up the
domains without a restart.
A tls_mgm row is identified by (domain, type), so defaulting the type
when it is not given lets these two commands change a different domain
than the intended one.  Require it for the commands that modify an
existing row; db_add and db_show keep falling back to tls_db_type.

Also guard against read_param() returning None when there is no
terminal to prompt on, which made the type default path raise an
AttributeError instead of reporting the missing value.
The type is part of the identity of a tls_mgm row, not an environment
setting, so it does not belong in opensips-cli.cfg next to the database
URL.  Ask for it instead, defaulting to 'server' on empty input; the
prompt already shows that default.

db_update and db_delete keep requiring it as an argument.
db_update and db_delete errored out when the type was missing, while the
domain right next to it was asked for, which is confusing when running
the commands interactively.

Ask for the type as well, and drop the default for these two commands
instead: read_param() keeps asking until a type is given, so they still
cannot pick a different row than the intended one.
db_list addresses no domain at all, so it never asks for a type.
'db_add a.example.org type=client' reported "unknown tls_mgm column
'type'", which is not true: the column exists, it just identifies the
row and is passed as an argument.  Name the three identity columns and
say so.

Parse the columns before resolving the domain as well, so that such a
command is rejected right away rather than after asking for the domain
and its type.
Rejecting 'domain=' and 'type=' meant the identity of a row was the one
thing that could not be written the way every other column is, which is
hard to justify to someone who just read the column list.

Take everything as 'column=value', keep the first two arguments as a
shorthand for the domain and its type, and refuse only the combination
of the two, which is the single ambiguous case.  Whatever is left out is
then asked for, in one place instead of one per spelling.

This folds tls_db_domain() into tls_db_params(), so db_show and
db_delete go through the same parser and now report the columns they do
not take.  'id' stays refused: the database generates it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant