Skip to content

Commit e632da2

Browse files
Copilotromanett
andauthored
Add provisioning mode support for certificate provisioning and initial server configuration (#3375)
* Initial plan * Add provisioning mode support to ConsoleReferenceServer Co-authored-by: romanett <[email protected]> * Add provisioning mode test and ServerFixture support Co-authored-by: romanett <[email protected]> * Add provisioning mode documentation Co-authored-by: romanett <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: romanett <[email protected]>
1 parent 8493e73 commit e632da2

File tree

7 files changed

+307
-16
lines changed

7 files changed

+307
-16
lines changed

Applications/ConsoleReferenceServer/Program.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ public static async Task<int> Main(string[] args)
6969
bool shadowConfig = false;
7070
bool samplingGroups = false;
7171
bool cttMode = false;
72+
bool provisioningMode = false;
7273
char[] password = null;
7374
int timeout = -1;
7475

@@ -92,7 +93,12 @@ public static async Task<int> Main(string[] args)
9293
"use the sampling group mechanism in the Reference Node Manager",
9394
sg => samplingGroups = sg != null
9495
},
95-
{ "ctt", "CTT mode, use to preset alarms for CTT testing.", c => cttMode = c != null }
96+
{ "ctt", "CTT mode, use to preset alarms for CTT testing.", c => cttMode = c != null },
97+
{
98+
"provision",
99+
"start server in provisioning mode with limited namespace for certificate provisioning",
100+
p => provisioningMode = p != null
101+
}
96102
};
97103

98104
using var telemetry = new ConsoleTelemetry();
@@ -155,6 +161,20 @@ await server
155161
// Create and add the node managers
156162
server.Create(Servers.Utils.NodeManagerFactories);
157163

164+
// enable provisioning mode if requested
165+
if (provisioningMode)
166+
{
167+
logger.LogInformation("Enabling provisioning mode.");
168+
Servers.Utils.EnableProvisioningMode(server.Server);
169+
// Auto-accept is required in provisioning mode
170+
if (!autoAccept)
171+
{
172+
logger.LogInformation("Auto-accept enabled for provisioning mode.");
173+
autoAccept = true;
174+
server.AutoAccept = autoAccept;
175+
}
176+
}
177+
158178
// enable the sampling groups if requested
159179
if (samplingGroups)
160180
{

Applications/Quickstarts.Servers/ReferenceServer/ReferenceServer.cs

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ public class ReferenceServer : ReverseConnectServer
6161
/// </summary>
6262
public bool UseSamplingGroupsInReferenceNodeManager { get; set; }
6363

64+
/// <summary>
65+
/// If true, the server starts in provisioning mode with limited namespace
66+
/// and requires authenticated user access for certificate provisioning
67+
/// </summary>
68+
public bool ProvisioningMode { get; set; }
69+
6470
/// <summary>
6571
/// Creates the node managers for the server.
6672
/// </summary>
@@ -77,25 +83,36 @@ protected override MasterNodeManager CreateMasterNodeManager(
7783
Utils.TraceMasks.StartStop,
7884
"Creating the Reference Server Node Manager.");
7985

80-
IList<INodeManager> nodeManagers =
81-
[
82-
// create the custom node manager.
83-
new ReferenceNodeManager(
84-
server,
85-
configuration,
86-
UseSamplingGroupsInReferenceNodeManager)
87-
];
86+
IList<INodeManager> nodeManagers;
87+
var asyncNodeManagers = new List<IAsyncNodeManager>();
8888

89-
foreach (INodeManagerFactory nodeManagerFactory in NodeManagerFactories)
89+
if (ProvisioningMode)
9090
{
91-
nodeManagers.Add(nodeManagerFactory.Create(server, configuration));
91+
m_logger.LogInformation(
92+
Utils.TraceMasks.StartStop,
93+
"Server is in provisioning mode - limited namespace enabled.");
94+
nodeManagers = [];
9295
}
93-
94-
var asyncNodeManagers = new List<IAsyncNodeManager>();
95-
96-
foreach (IAsyncNodeManagerFactory nodeManagerFactory in AsyncNodeManagerFactories)
96+
else
9797
{
98-
asyncNodeManagers.Add(nodeManagerFactory.CreateAsync(server, configuration).AsTask().GetAwaiter().GetResult());
98+
nodeManagers =
99+
[
100+
// create the custom node manager.
101+
new ReferenceNodeManager(
102+
server,
103+
configuration,
104+
UseSamplingGroupsInReferenceNodeManager)
105+
];
106+
107+
foreach (INodeManagerFactory nodeManagerFactory in NodeManagerFactories)
108+
{
109+
nodeManagers.Add(nodeManagerFactory.Create(server, configuration));
110+
}
111+
112+
foreach (IAsyncNodeManagerFactory nodeManagerFactory in AsyncNodeManagerFactories)
113+
{
114+
asyncNodeManagers.Add(nodeManagerFactory.CreateAsync(server, configuration).AsTask().GetAwaiter().GetResult());
115+
}
99116
}
100117

101118
return new MasterNodeManager(server, configuration, null, asyncNodeManagers, nodeManagers);
@@ -227,6 +244,12 @@ public override UserTokenPolicyCollection GetUserTokenPolicies(
227244
configuration,
228245
description);
229246

247+
// In provisioning mode, remove anonymous authentication
248+
if (ProvisioningMode)
249+
{
250+
return [.. policies.Where(u => u.TokenType != UserTokenType.Anonymous)];
251+
}
252+
230253
// sample how to modify default user token policies
231254
if (description.SecurityPolicyUri == SecurityPolicies.Aes256_Sha256_RsaPss &&
232255
description.SecurityMode == MessageSecurityMode.SignAndEncrypt)

Applications/Quickstarts.Servers/Utils.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,15 @@ public static void UseSamplingGroupsInReferenceNodeManager(
114114
server.UseSamplingGroupsInReferenceNodeManager = true;
115115
}
116116

117+
/// <summary>
118+
/// Enable provisioning mode in the ReferenceServer.
119+
/// </summary>
120+
public static void EnableProvisioningMode(
121+
ReferenceServer.ReferenceServer server)
122+
{
123+
server.ProvisioningMode = true;
124+
}
125+
117126
/// <summary>
118127
/// The property with available node manager factories.
119128
/// </summary>

Docs/ProvisioningMode.md

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# Provisioning Mode
2+
3+
## Overview
4+
5+
Provisioning mode is a special server startup mode designed to facilitate secure certificate provisioning and initial server configuration. When enabled, the server starts with a limited default namespace and enhanced security settings to allow administrators to safely configure the server before exposing the full application functionality.
6+
7+
## Purpose
8+
9+
The provisioning mode addresses the need for secure server initialization by:
10+
- Providing a minimal server environment during initial setup
11+
- Requiring authenticated user access (anonymous access is disabled)
12+
- Enabling automatic certificate acceptance to simplify certificate provisioning
13+
- Preventing access to application-specific data and functionality until the server is properly configured
14+
15+
This mode is particularly useful when:
16+
- Setting up a new OPC UA server installation
17+
- Provisioning certificates for production environments
18+
- Performing initial server configuration in a secure manner
19+
20+
## How to Enable Provisioning Mode
21+
22+
### ConsoleReferenceServer
23+
24+
The ConsoleReferenceServer application supports provisioning mode via the `--provision` command line option:
25+
26+
```bash
27+
dotnet ConsoleReferenceServer.dll --provision
28+
```
29+
30+
### Command Line Options
31+
32+
When using provisioning mode, you can combine it with other options:
33+
34+
```bash
35+
# Enable provisioning mode with console logging
36+
dotnet ConsoleReferenceServer.dll --provision --console --log
37+
38+
# Enable provisioning mode with timeout
39+
dotnet ConsoleReferenceServer.dll --provision --timeout 300
40+
```
41+
42+
Note: When provisioning mode is enabled, auto-accept for untrusted certificates is automatically activated. You don't need to specify the `--autoaccept` option separately.
43+
44+
## Behavior in Provisioning Mode
45+
46+
When the server starts in provisioning mode:
47+
48+
### 1. Limited Namespace
49+
- Only the core OPC UA namespace is exposed
50+
- Application-specific node managers are not loaded
51+
- Standard OPC UA objects (Objects, Types, Views) remain accessible
52+
- Custom server nodes and data are not available
53+
54+
### 2. Authentication Requirements
55+
- Anonymous authentication is disabled
56+
- Users must authenticate using:
57+
- Username/password
58+
- X.509 certificate
59+
- Other supported authentication mechanisms
60+
61+
### 3. Certificate Handling
62+
- Auto-accept for untrusted certificates is enabled
63+
- Administrators can provision certificates without manual approval
64+
- Certificate validation still occurs, but untrusted certificates are accepted
65+
66+
### 4. Reduced Attack Surface
67+
- No application-specific functionality is exposed
68+
- Minimal node managers are active
69+
- Only core server capabilities are available
70+
71+
## Use Cases
72+
73+
### Initial Server Setup
74+
1. Start the server in provisioning mode
75+
2. Connect as an authenticated administrator
76+
3. Provision required certificates
77+
4. Configure server settings
78+
5. Restart the server in normal mode
79+
80+
### Certificate Provisioning
81+
1. Start the server with `--provision`
82+
2. Use a GDS (Global Discovery Server) or certificate management tool
83+
3. Push certificates to the server
84+
4. Verify certificate installation
85+
5. Exit provisioning mode
86+
87+
### Secure Configuration
88+
1. Enable provisioning mode for configuration changes
89+
2. Make necessary updates to server configuration
90+
3. Test changes in the limited environment
91+
4. Restart in production mode
92+
93+
## Implementation Details
94+
95+
### ReferenceServer Class
96+
97+
The provisioning mode is implemented in the `ReferenceServer` class:
98+
99+
```csharp
100+
public bool ProvisioningMode { get; set; }
101+
```
102+
103+
When `ProvisioningMode` is true:
104+
- `CreateMasterNodeManager` skips loading application-specific node managers
105+
- `GetUserTokenPolicies` removes anonymous authentication policies
106+
107+
### Programmatic Usage
108+
109+
To enable provisioning mode programmatically:
110+
111+
```csharp
112+
var server = new ReferenceServer();
113+
Quickstarts.Servers.Utils.EnableProvisioningMode(server);
114+
```
115+
116+
Or directly:
117+
118+
```csharp
119+
var server = new ReferenceServer
120+
{
121+
ProvisioningMode = true
122+
};
123+
```
124+
125+
## Security Considerations
126+
127+
### Benefits
128+
- Reduces attack surface during initial setup
129+
- Requires authenticated access for configuration
130+
- Prevents unauthorized access to application data
131+
132+
### Limitations
133+
- Auto-accept mode accepts all certificates (including potentially malicious ones)
134+
- Should only be used during controlled setup/configuration periods
135+
- Not intended for long-term operation
136+
137+
### Best Practices
138+
1. Use provisioning mode only during initial setup or maintenance windows
139+
2. Ensure the server is not accessible from untrusted networks while in provisioning mode
140+
3. Switch to normal mode as soon as provisioning is complete
141+
4. Monitor and review all certificates that were auto-accepted
142+
5. Remove or properly validate any questionable certificates before exiting provisioning mode
143+
144+
## Comparison with Normal Mode
145+
146+
| Feature | Normal Mode | Provisioning Mode |
147+
|---------|-------------|-------------------|
148+
| Namespace | Full application namespace | Core namespace only |
149+
| Node Managers | All configured | Core managers only |
150+
| Anonymous Access | Allowed (if configured) | Disabled |
151+
| Auto-Accept Certificates | Optional | Enabled |
152+
| Application Data | Accessible | Not available |
153+
| Security Admin Access | Available | Required |
154+
155+
## Examples
156+
157+
### Start Server in Provisioning Mode
158+
159+
Windows:
160+
```cmd
161+
ConsoleReferenceServer.exe --provision --console --log
162+
```
163+
164+
Linux/Mac:
165+
```bash
166+
dotnet ConsoleReferenceServer.dll --provision --console --log
167+
```
168+
169+
### Expected Output
170+
171+
When provisioning mode is active, you should see log messages similar to:
172+
173+
```
174+
[INF] Enabling provisioning mode.
175+
[INF] Auto-accept enabled for provisioning mode.
176+
[INF] Server is in provisioning mode - limited namespace enabled.
177+
[INF] MasterNodeManager.Startup - NodeManagers=2
178+
```
179+
180+
The `NodeManagers=2` indicates only the core node managers are loaded (CoreNodeManager and SystemNodeManager), confirming the limited namespace is in effect.
181+
182+
## Related Documentation
183+
184+
- [Certificates](Certificates.md) - Certificate management and storage
185+
- [RoleBasedUserManagement](RoleBasedUserManagement.md) - User authentication and authorization

Docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ UA Core stack related:
1616
Reference application related:
1717
* [Reference Client](../Applications/ConsoleReferenceClient/README.md) documentation for configuration of the console reference client using parameters.
1818
* [Reference Server](../Applications/README.md) documentation for running against CTT.
19+
* [Provisioning Mode](ProvisioningMode.md) for secure certificate provisioning and initial server configuration.
1920
* Using the [Container support](ContainerReferenceServer.md) of the Reference Server in Visual Studio 2026 and for local testing.
2021

2122
Starting with version 1.5.375.XX the winforms reference client & winforms reference server were moved to the [OPC UA .NET Standard Samples](https://github.com/OPCFoundation/UA-.NETStandard-Samples) repository.

Tests/Opc.Ua.Server.Tests/ReferenceServerTest.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -981,5 +981,52 @@ public async Task ServerEventNotifierHistoryReadBitAsync()
981981
Assert.IsTrue((eventNotifier & EventNotifiers.SubscribeToEvents) != 0,
982982
"Server EventNotifier should have SubscribeToEvents bit set");
983983
}
984+
985+
/// <summary>
986+
/// Test provisioning mode - server should start with limited namespace.
987+
/// </summary>
988+
[Test]
989+
public async Task ProvisioningModeTestAsync()
990+
{
991+
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
992+
993+
// start Ref server in provisioning mode
994+
var fixture = new ServerFixture<ReferenceServer>
995+
{
996+
AllNodeManagers = false,
997+
OperationLimits = false,
998+
DurableSubscriptionsEnabled = false,
999+
AutoAccept = true,
1000+
ProvisioningMode = true
1001+
};
1002+
1003+
ReferenceServer server = await fixture.StartAsync().ConfigureAwait(false);
1004+
1005+
// Verify provisioning mode is enabled
1006+
Assert.IsTrue(server.ProvisioningMode, "Server should be in provisioning mode");
1007+
1008+
// Get endpoints - in provisioning mode, anonymous authentication should not be allowed
1009+
EndpointDescriptionCollection endpoints = server.GetEndpoints();
1010+
Assert.IsNotNull(endpoints);
1011+
Assert.IsTrue(endpoints.Count > 0, "Server should have endpoints");
1012+
1013+
// Check that anonymous token policy is not present for at least one endpoint
1014+
bool hasEndpointWithoutAnonymous = false;
1015+
foreach (EndpointDescription endpoint in endpoints)
1016+
{
1017+
bool hasAnonymous = endpoint.UserIdentityTokens.Any(
1018+
policy => policy.TokenType == UserTokenType.Anonymous);
1019+
if (!hasAnonymous)
1020+
{
1021+
hasEndpointWithoutAnonymous = true;
1022+
break;
1023+
}
1024+
}
1025+
Assert.IsTrue(hasEndpointWithoutAnonymous,
1026+
"At least one endpoint should not allow anonymous authentication in provisioning mode");
1027+
1028+
// Clean up
1029+
await fixture.StopAsync().ConfigureAwait(false);
1030+
}
9841031
}
9851032
}

0 commit comments

Comments
 (0)