Quick Setup#

The quick setup gives you a secure server and client with a few lines, and an error message that names the setting that is wrong. It adds no protocol behaviour: it collects the settings that are otherwise many Server setters and Client calls into configuration objects, and it names three ready-made combinations, the profiles. The setters stay available as the low-level API.

A secure pair#

Server.quickstart() and Client.quickstart() return a configured object that is not yet started. The PKI directory is created on await server.init() (the client creates its own at the first connect()), each with a self-signed application certificate whose ApplicationUri matches the configuration. To have the certificate earlier, for example to hand it to the other side before anything connects, call await client.init_pki() or await server.init_pki(): it creates the directory and the certificate, returns the certificate and can be called again.

Nothing is trusted automatically. Before a secure connection can work, each side has to trust the other’s certificate; this is the one step that cannot be skipped. The example does it in code; on a command line you use enginir-opcua pair (below).

01_secure_pair.py#
async def main() -> None:
    with workdir() as directory:
        url = f"opc.tcp://127.0.0.1:{free_port()}/enginir-opcua/example/"
        server = Server.quickstart(url, pki=directory / "server-pki")
        client = Client.quickstart(url, pki=directory / "client-pki")

        await server.init()
        await client.init_pki()
        trust(directory / "server-pki", directory / "client-pki")
        trust(directory / "client-pki", directory / "server-pki")

        idx = await server.register_namespace("urn:enginir:example:secure-pair")
        temperature = await server.nodes.objects.add_variable(idx, "Temperature", 21.5)
        async with server, client:
            endpoint = client.security_policy.URI.rsplit("#", 1)[-1]
            print("secure channel with", endpoint)
            print("temperature:", await client.get_node(temperature.nodeid).read_value())
    print("OK 01_secure_pair")

The runnable file, with its imports, is 01_secure_pair.py. The two minimal pages that follow, A Minimal OPC-UA Server and A Minimal OPC-UA Client, split it into two programs.

Profiles#

secure (default)

development

legacy

Server security

Basic256Sha256, Aes128_Sha256_RsaOaep and Aes256_Sha256_RsaPss, all with SignAndEncrypt only

No security, and Basic256Sha256 with SignAndEncrypt

The secure policies in Sign and SignAndEncrypt, Basic256 and Basic128Rsa15, and no security

Client security

Basic256Sha256, SignAndEncrypt

No security

auto: the strongest endpoint the server offers, falling back to no security

Network

Any address

Loopback only (127.0.0.1, ::1, localhost); another host is a ConfigurationError

Any address

Certificate checks

Nothing relaxed

Revocation and host name errors are accepted

The same

Passwords in clear text

Refused

Allowed

Refused

Start

Quiet

Logs a warning for every weaker setting

The same

Trust

Nothing trusted until you say so

Nothing trusted for the secured endpoint; the unsecured one needs no certificate

Nothing trusted for the secured endpoints

Users

A users file (below); anonymous sessions are read-only

The same

The same

Use development only on your own machine, to try things out, and legacy only for devices that cannot do better. Both are your decision: nothing is blocked, and every weaker setting is logged when the server starts or the client connects. Security Profiles has the complete tables, the settings and the log lines. The examples are 15_development_profile.py and 17_legacy_profile.py.

Settings and configuration objects#

Everything else is a keyword argument of quickstart: Server.quickstart(endpoint, profile="secure", pki="./pki", users=None, **overrides) and Client.quickstart(url, profile="secure", pki="./pki", username=None, password=None, **overrides). An override is any field of ServerConfig or ClientConfig, for example name, application_uri, security, limits=ServerLimits(max_sessions=10) or auto_reconnect=True. An unknown name is an error.

A configuration is validated as a whole, and every error names the setting as it is written in the TOML file:

ConfigurationError: server.endpoint: the development profile only listens on a loopback address, got
'opc.tcp://0.0.0.0:4840/dev/'; use 127.0.0.1 or localhost, or profile "secure"

The same objects can be built directly and passed to the constructors, Server(config=...) and Client(config=...). A config replaces the other constructor arguments; mixing them is an error.

Configuration files#

ServerConfig.from_toml(path) and ClientConfig.from_toml(path) read a TOML file with the standard library’s tomllib. Unknown keys are errors, relative paths in the file are relative to the file, and secrets never live in it: a password comes from the environment variable named by password_env (default ENGINIR_OPCUA_PASSWORD), and a users file is found through users_file or ENGINIR_OPCUA_USERS_FILE. config.dump(path) writes the file with a comment for every setting; enginir-opcua init does it for you (--profile secure|development|legacy), and for a server it also creates an empty users.toml that users_file names:

[server]
profile = "secure"
endpoint = "opc.tcp://127.0.0.1:4840/enginir-opcua/server/"
application_uri = "urn:enginir:opcua:server"
pki = "pki"
users_file = "users.toml"

[server.limits]
max_sessions = 100
from enginir_opcua import Server
from enginir_opcua.config import ServerConfig

server = Server(config=ServerConfig.from_toml("server.toml"))
await server.init()

enginir-opcua server --config server.toml serves the same file without code. push_management = true makes init() switch on certificate management over OPC UA (OPC 10000-12), so a SecurityAdmin can replace the certificate and the trust lists remotely.

Redundant servers and the synchronous API#

HaClient.quickstart(urls, profile="secure", pki="./pki", username=None, password=None, **overrides) gives a client for a redundant set the same quick setup: all its clients share the PKI directory and the host-derived ApplicationUri, and await ha.init_pki() creates the certificate before start(). See 09_ha_client.py.

enginir_opcua.sync.Server.quickstart and enginir_opcua.sync.Client.quickstart have the same arguments and one more, tloop, to run both on one ThreadLoop; the sync client has init_pki() too. See 11_sync.py.

Users and roles#

A server that should accept more than anonymous read access takes a users file: a TOML file with a name, a role and a salted scrypt hash for each user. Plain-text passwords are never stored.

$ enginir-opcua user add users.toml alice --role user
added user 'alice' (role user) to users.toml
$ enginir-opcua user list users.toml
alice user

The roles are user, admin and security_admin. Anonymous sessions have the read-only role and can read, browse, subscribe and read history. A user may also write and call methods, an admin may also change the address space, and a security_admin may also manage certificates and read the security diagnostics. The file is read again when it changes. In code, write_users, hash_password and UserRecord in enginir_opcua.crypto.users_file write the same file, and FileUserManager(path, anonymous=True) is the user manager behind Server.quickstart(users=path). See 02_users_and_roles.py.

The trust commands#

The program enginir-opcua has five commands for setting up the trust between two applications. They work on PKI directories (see Certificates: the PKI directory).

enginir-opcua init server|client DIR

Create DIR/pki with a self-signed application certificate, and a commented DIR/server.toml or DIR/client.toml; for a server also an empty DIR/users.toml, which the configuration names. Options: --endpoint (--url for a client), --profile, --name, --application-uri, --force. An existing certificate or users file is never replaced. A server on every interface (0.0.0.0) gets a certificate that names the host’s IP addresses, so clients can connect by address.

enginir-opcua pki list [--pki DIR]

Show the folders of a PKI directory with the subject, thumbprint and expiry of every certificate.

enginir-opcua trust THUMBPRINT_OR_FILE [--pki DIR]

Trust a certificate: move a rejected one, by thumbprint, from rejected/certs to trusted/certs, or add a certificate file. A server or client that refuses a peer tells you the command to run, with the thumbprint.

enginir-opcua pair SERVER_PKI CLIENT_PKI

Copy each certificate into the other directory’s trusted/certs. It prints a warning: nobody checks who holds the key, so it is for development on one machine. On a network, compare the thumbprints yourself and use trust.

enginir-opcua user add|list|remove FILE

Manage a users file, see above. add asks for the password; --password-stdin reads one line from standard input.

A complete session on one machine:

$ enginir-opcua init server srv --endpoint opc.tcp://127.0.0.1:4840/enginir-opcua/server/
created  srv/pki
created  srv/pki/own/certs/application.der  thumbprint 584532607f12...  expires 2027-10-29
created  srv/server.toml
created  srv/users.toml
next     enginir-opcua user add srv/users.toml <name> --role user
$ enginir-opcua init client cli --url opc.tcp://127.0.0.1:4840/enginir-opcua/server/
$ enginir-opcua pair srv/pki cli/pki
trusted  301d083dbf7e...  CN=urn:host:enginir-opcua:client  in srv/pki/trusted/certs
trusted  584532607f12...  CN=urn:host:enginir-opcua:server  in cli/pki/trusted/certs
$ enginir-opcua user add srv/users.toml alice --role user

If a peer’s certificate has not been trusted, both sides say which, and how. The server logs:

192.168.0.20:51234 refused: client certificate 8b66...a1e (CN=urn:host:enginir-opcua:client) rejected: it is not
trusted (BadCertificateUntrusted); trust it with: enginir-opcua trust 8b66...a1e --pki srv/pki

and the client’s error names both commands when neither side trusts the other yet:

the server refused the secure channel, probably because it does not trust this client's certificate 8b66...a1e;
trust it on the server: enginir-opcua trust 8b66...a1e --pki <server pki>. This client does not trust the server
either: server certificate 5845...12f (CN=urn:host:enginir-opcua:server) is not trusted; trust it with:
enginir-opcua trust 5845...12f --pki cli/pki

A SecurityAdmin can do the same over OPC UA with the push model of OPC 10000-12; see 14_certificate_management.py.

Examples#

The examples directory of the repository has one runnable file per task, from this secure pair to alarms, history, file transfer and reverse connect. Examples describes each of them.