The OPC UA data plane is machine-to-machine. A client connects with an OPC UA
UserName token whose username is the literal apikey and
whose password is the key below. The key resolves to a principal; ACL rows naming
that principal are what grant it permissions.
Revocation is not uniform across the fleet. A node with a config database (a fused
admin+driver node such as this one) applies it immediately, because it reads keys per
request. A driver-only node has no config database, so its keys travel inside the
deployment artifact and a revocation reaches it only on the next deployment.
Assume the slower behaviour when revoking a compromised key.
@if (_created is not null)
{
Copy this key now — it is not stored and cannot be shown again.
@_created
}
@if (!string.IsNullOrWhiteSpace(_error))
{
@_error
}
Create a key
Keys
@if (_keys is null)
{
Loading…
}
else if (_keys.Count == 0)
{
No keys yet. Until one exists, only anonymous sessions can connect.
}
else
{
Name
Principal
Key id
Status
Created (UTC)
Expires (UTC)
@foreach (var k in _keys)
{
@k.Name
@k.Principal
@k.ApiKeyId
@if (!k.Enabled)
{
revoked
}
else if (k.ExpiresUtc is { } e && e <= DateTime.UtcNow)
{
expired
}
else
{
active
}
@k.CreatedUtc.ToString("yyyy-MM-dd HH:mm")
@(k.ExpiresUtc?.ToString("yyyy-MM-dd") ?? "—")
}
}
@code {
private IReadOnlyList? _keys;
private string _newName = "";
private string _newPrincipal = "";
private DateTime? _newExpires;
private string? _created;
private string? _error;
private bool _busy;
protected override async Task OnInitializedAsync() => await ReloadAsync();
private async Task ReloadAsync()
{
try
{
_keys = await Keys.ListAsync(default);
}
catch (Exception ex)
{
_error = ex.Message;
_keys = [];
}
}
private async Task CreateAsync()
{
_busy = true;
_error = null;
_created = null;
try
{
// Shown once, held only in this component's state. There is no way to recover it afterwards
// — the server stores a hash — so it is deliberately not written anywhere else.
_created = await Keys.CreateAsync(
_newName,
_newPrincipal,
// A date-only input means midnight; treat it as UTC rather than local so an operator
// near a timezone boundary does not get a key that expired before it was issued.
_newExpires is { } d ? DateTime.SpecifyKind(d, DateTimeKind.Utc) : null,
notes: null,
default);
_newName = "";
_newPrincipal = "";
_newExpires = null;
await ReloadAsync();
}
catch (Exception ex)
{
_error = ex.Message;
}
finally
{
_busy = false;
}
}
private async Task SetEnabledAsync(string apiKeyId, bool enabled)
{
_busy = true;
_error = null;
try
{
await Keys.SetEnabledAsync(apiKeyId, enabled, default);
await ReloadAsync();
}
catch (Exception ex) { _error = ex.Message; }
finally { _busy = false; }
}
private async Task DeleteAsync(string apiKeyId)
{
_busy = true;
_error = null;
try
{
await Keys.DeleteAsync(apiKeyId, default);
await ReloadAsync();
}
catch (Exception ex) { _error = ex.Message; }
finally { _busy = false; }
}
}