Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CloudLibSyncAzureFunction/CloudLibSyncAzureFunction.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Update="Microsoft.SourceLink.GitHub" Version="10.0.103">
<PackageReference Update="Microsoft.SourceLink.GitHub" Version="10.0.300">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Update="Nerdbank.GitVersioning" Version="3.9.50" />
<PackageReference Update="Nerdbank.GitVersioning" Version="3.10.85" />
</ItemGroup>
</Project>
25 changes: 10 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,25 +358,20 @@ The UA Cloud Library database configuration is documented in the [Database Setup
To migrate from version 1.0 to version 1.1, you need to update your database as V1.1 no longer requires blob storage. We provided a command line tool called BlobToPGTable in this repository for the major clouds which will complete this step for you.

### Required Settings - PostgreSQL
You **must** have installed PostgreSQL version 11.20 or higher. You **must** also define one of the following two sets of environment variables:
You **must** have installed PostgreSQL version 11.20 or higher. You **must** also define the following environment variable:

#### PostgreSQL Set 1: Three environment variables
* `PostgreSQLEndpoint`: The endpoint of the PostgreSQL instance (that must be previously deployed in the hosting platform).
* `PostgreSQLUsername`: The username to use to log in to the PostgreSQL instance.
* `PostgreSQLPassword`: The password to use to log in to the PostgreSQL instance.

#### PostgreSQL Set 2: One connection string
* `ConnectionStrings__CloudLibraryPostgreSQL`: All of the above values, as a connection string instead of as individual environment variables. Example:
* `ConnectionStrings__CloudLibraryPostgreSQL`: A full Npgsql connection string to the PostgreSQL instance (that must be previously deployed in the hosting platform). Example:
```
"Server=localhost; Username=MyUserName;Password=MyUserPassword;Database=uacloudlib;Port=5432;Include Error Detail=true",
"Server=localhost;Username=MyUserName;Password=MyUserPassword;Database=uacloudlib;Port=5432;Ssl Mode=Require;Include Error Detail=true",
```
**Note: that you must create a user account with set privileges to access the database.**
**Note: you must create a user account with set privileges to access the database, and set `Ssl Mode=Require` for any non-loopback server — most managed PostgreSQL offerings (including Azure Database for PostgreSQL) reject unencrypted connections outright.**

### Setting Password for Admin Account
### Setting Credentials for Admin Account
To enable access, from both Swagger and the REST API, you must set a password using this environment variable:
* `ServicePassword`: The administration password for Swagger and REST service.

**Note: The user name is `admin`.**
Optionally, set the admin user name as well:
* `ServiceUsername`: The administration user name for Swagger and REST service (default: `admin`).

### Optional Settings
Environment variables that **can optionally** be defined:
Expand Down Expand Up @@ -416,17 +411,17 @@ The following STRIDE-based threat model covers the `UACloudLibraryServer` projec
| 2 | **S**poofing | Interactive UI / Identity area (`Areas/Identity/Pages/Account/*`) | An attacker creates an account using a victim's email address or hijacks a session. | ASP.NET Core Identity is used with confirmed-account sign-in (`RequireConfirmedAccount = true` whenever `EmailSenderAPIKey` is configured), email confirmation flow (`ConfirmEmail`, `ConfirmEmailChange`), password reset confirmation, lockout (`Lockout.cshtml`) and optional Google reCAPTCHA (`CaptchaValidation`) on registration. External identity providers (Microsoft Account, Azure Entra ID via `Microsoft.Identity.Web`, OPC Foundation OAuth2) are wired through `AddAuthentication()` in `Startup` so federated MFA can be enforced at the IdP. |
| 3 | **S**poofing | Service-to-service callers using API keys | A leaked or guessed key is replayed against the API. | API keys are issued per user via `ApiKeyTokenProvider` (registered through Identity's token-provider pipeline) and validated by `UserService.ValidateApiKeyAsync` from `ApiKeyAuthenticationHandler`. Keys are bound to the issuing Identity user, can be revoked from `ManageApiKeys.cshtml`, are transmitted only via the dedicated `X-API-Key` header (declared as the Swagger `ApiKeyAuth` security scheme) and are only honoured when the operator has explicitly opted-in via the `APIKeyAuth` environment variable. |
| 4 | **T**ampering | Inbound nodeset / values / DPP file uploads (`UploadController`, `DPPLifecycleApiController`, `AssetAdministrationShellEnvironmentService`) | A caller submits a malformed or malicious file (XXE, oversized payload, executable disguised as XML/JSON) to corrupt the library or trigger code execution. | `UploadController.UploadNodeset` validates that `nodesetFile.ContentType == "text/xml"` and `values.ContentType == "text/json"`, rejects empty payloads, wraps file names in `FileInfo` for path-character validation and persists content as text. Nodeset XML is parsed through the OPC Foundation `Opc.Ua.Configuration` / `NodesetModelFactoryOpc` pipeline which uses safe XML readers. All metadata fields (title, license, copyright, description, URLs) are individually validated before reaching `CloudLibDataProvider.UploadNamespaceAndNodesetAsync`. Operators should additionally configure Kestrel/IIS request-body size limits and front the service with a WAF. |
| 5 | **T**ampering | Database persistence (`AppDbContext`, `CloudLibDataProvider`, `DbFileStorage`) | SQL injection or direct DB tampering modifies stored nodesets, users or roles. | EF Core (`Microsoft.EntityFrameworkCore` / Npgsql) is used everywhere – all queries are parameterised LINQ. Schema is managed exclusively through versioned EF Core migrations under `Migrations/`. PostgreSQL credentials are taken from environment variables (`PostgreSQLEndpoint/Username/Password` or `ConnectionStrings__CloudLibraryPostgreSQL`) and never hard-coded. Operators are expected to grant the application a least-privilege DB role and to keep PostgreSQL ≥ 11.20. |
| 5 | **T**ampering | Database persistence (`AppDbContext`, `CloudLibDataProvider`, `DbFileStorage`) | SQL injection or direct DB tampering modifies stored nodesets, users or roles. | EF Core (`Microsoft.EntityFrameworkCore` / Npgsql) is used everywhere – all queries are parameterised LINQ. Schema is managed exclusively through versioned EF Core migrations under `Migrations/`. PostgreSQL credentials are taken from the `ConnectionStrings__CloudLibraryPostgreSQL` environment variable and never hard-coded. Operators are expected to grant the application a least-privilege DB role and to keep PostgreSQL ≥ 11.20. |
| 6 | **T**ampering | Data-protection keys & cookies | An attacker who reads the key ring forges authentication cookies or anti-forgery tokens. | `Startup.ConfigureServices` calls `services.AddDataProtection().PersistKeysToFileSystem(...)` so keys are persisted (and can be mounted on a protected volume in container deployments). External-login correlation cookies are pinned to `SameSite=Strict` and `CookieSecurePolicy.Always`, and the entire pipeline runs behind `app.UseHttpsRedirection()`. ASP.NET Core's automatic anti-forgery token validation is active for the Razor Pages / Blazor UI. |
| 7 | **R**epudiation | Administrative actions (approve / delete nodesets, manage users, issue API keys) | A user denies performing a destructive action because actions are not auditable. | All privileged endpoints sit behind authenticated identities (Identity user or federated principal) so every request is bound to a `User.Identity.Name`. The upload pipeline records the uploader's identity (`_database.UploadNamespaceAndNodesetAsync(User.Identity.Name, ...)`). Application logging is enabled via `services.AddLogging(builder => builder.AddConsole())` and emits structured logs that can be shipped to a central SIEM/Log Analytics workspace from the container host. |
| 8 | **R**epudiation | External OAuth callback (`/Account/ExternalLogin`, `OAuthEvents.OnCreatingTicket`) | A replayed or forged ticket is accepted as a legitimate sign-in. | The OAuth handler enforces correlation cookies (`CorrelationCookie.SameSite = Strict`, `SecurePolicy = Always`), uses HTTPS-only token endpoints, calls `EnsureSuccessStatusCode()` on the userinfo response, and stamps a `TicketCreated` token into the authentication properties so the time of issuance is preserved alongside the access token. |
| 9 | **I**nformation disclosure | Stored user secrets (passwords, API keys, external tokens) | DB compromise leaks credentials usable elsewhere. | Passwords are stored as PBKDF2 hashes by ASP.NET Core Identity (`AddDefaultIdentity<IdentityUser>`). API keys are issued through `ApiKeyTokenProvider` (an Identity `IUserTwoFactorTokenProvider`) and validated server-side by `UserService.ValidateApiKeyAsync`; they are not echoed back to the user after creation. OAuth refresh/access tokens stored via `SaveTokens = true` are protected by ASP.NET Core Data Protection. |
| 10 | **I**nformation disclosure | Configuration / secrets surface | Secrets such as `ServicePassword`, `EmailSenderAPIKey`, `OAuth2ClientSecret`, `Authentication:Microsoft:ClientSecret`, `CaptchaSettings__SecretKey` and the PostgreSQL password leak via source control or logs. | All secrets are read from `IConfiguration` (environment variables / mounted secret stores) and are never committed to the repository. The README explicitly documents the env-var contract (`ServicePassword`, `EmailSenderAPIKey`, `Authentication:Microsoft:ClientSecret`, `OAuth2ClientSecret`, `CaptchaSettings__SecretKey`, `PostgreSQLPassword`, `ConnectionStrings__CloudLibraryPostgreSQL`). The development-only exception page is gated by `env.IsDevelopment()` so stack traces are not returned in production. |
| 10 | **I**nformation disclosure | Configuration / secrets surface | Secrets such as `ServicePassword`, `EmailSenderAPIKey`, `OAuth2ClientSecret`, `Authentication:Microsoft:ClientSecret`, `CaptchaSettings__SecretKey` and the PostgreSQL password leak via source control or logs. | All secrets are read from `IConfiguration` (environment variables / mounted secret stores) and are never committed to the repository. The README explicitly documents the env-var contract (`ServicePassword`, `EmailSenderAPIKey`, `Authentication:Microsoft:ClientSecret`, `OAuth2ClientSecret`, `CaptchaSettings__SecretKey`, `ConnectionStrings__CloudLibraryPostgreSQL`). The development-only exception page is gated by `env.IsDevelopment()` so stack traces are not returned in production. |
| 11 | **I**nformation disclosure | Network traffic to/from the server | Credentials, cookies or API keys captured on the wire. | `app.UseHttpsRedirection()` forces TLS for every request. External login cookies are marked `Secure`. Containers are expected to be fronted by a TLS-terminating reverse proxy / load balancer. The embedded OPC UA `SimpleServer` (`UAClientServer/SimpleServer.cs`) uses the standard `Opc.Ua.Configuration.ApplicationInstance` certificate store so that the `opc.tcp` channel is signed and encrypted. |
| 12 | **D**enial of service | Public registration / login / password-reset endpoints | Bots flood self-registration, exhaust the email quota, or brute-force passwords. | Self-registration can be disabled entirely via `AllowSelfRegistration=false`. Google reCAPTCHA v3 is enforced through `CaptchaValidation` (configurable score via `CaptchaSettings__BotThreshold`) on registration. Identity's built-in lockout (`Lockout.cshtml`) blocks password brute-force. Email sending is delegated to Postmark or SendGrid (`PostmarkEmailSender`, `SendGridEmailSender`) which apply provider-side rate limits. |
| 13 | **D**enial of service | Large or malicious uploads, expensive nodeset parsing | A caller uploads many huge nodesets to fill storage or pin CPU. | Upload endpoints require an authenticated identity (`ApiPolicy`) so anonymous flooding is not possible. Uploaded nodesets are streamed through `MemoryStream` and then handed to `CloudLibDataProvider.UploadNamespaceAndNodesetAsync` which deduplicates by deterministic hash (`DeterministicHash.cs`) and stores them in PostgreSQL via `DbFileStorage`. Operators should additionally configure Kestrel (`KestrelServerOptions`) request-body size limits and HTTP timeouts at the reverse proxy. |
| 14 | **D**enial of service | Embedded OPC UA server (`UAClientServer/SimpleServer.cs`, `NodesetFileNodeManager.cs`) | A malicious OPC UA client opens excessive sessions/subscriptions or sends malformed messages. | The server is built on the OPC Foundation `Opc.Ua.Server` stack which enforces session limits, message size limits and security-policy validation through the configured `ApplicationInstance`. The OPC UA application certificate is created and validated automatically by `ApplicationInstance` so unsigned channels are rejected. |
| 15 | **E**levation of privilege | Administrative endpoints (approval, user/role management) | A regular user escalates to administrator and approves or deletes arbitrary nodesets. | Administrative operations are protected by the `AdministrationPolicy` defined in `Startup.ConfigureServices` (`policy.RequireRole("Administrator")`). The `Administrator` role can only be assigned by an existing administrator via the management UI, and the bootstrap admin password is supplied out-of-band via the `ServicePassword` environment variable (the user name is fixed to `admin`). API-key principals only carry the claims of the user that minted them, so a compromised key cannot exceed that user's role set. |
| 15 | **E**levation of privilege | Administrative endpoints (approval, user/role management) | A regular user escalates to administrator and approves or deletes arbitrary nodesets. | Administrative operations are protected by the `AdministrationPolicy` defined in `Startup.ConfigureServices` (`policy.RequireRole("Administrator")`). The `Administrator` role can only be assigned by an existing administrator via the management UI, and the bootstrap admin credentials are supplied out-of-band via the `ServicePassword` (and optional `ServiceUsername`, default `admin`) environment variables. API-key principals only carry the claims of the user that minted them, so a compromised key cannot exceed that user's role set. |
| 16 | **E**levation of privilege | Authentication-handler bypass | A bug in a custom authentication handler grants access without valid credentials. | The custom handlers (`BasicAuthenticationHandler`, `SignedInUserAuthenticationHandler`, `ApiKeyAuthenticationHandler`) all delegate credential verification to `UserService` which uses the Identity `UserManager`/`SignInManager` APIs (PBKDF2 password verification, normalised user lookup, time-constant comparisons). Authentication failures consistently return `AuthenticateResult.Fail/NoResult` and never short-circuit the pipeline as success. The combined `ApiPolicy` requires `RequireAuthenticatedUser()` so a `NoResult` from one scheme cannot be interpreted as success. |

### API Key Security Features
Expand Down
7 changes: 7 additions & 0 deletions UA-CloudLibrary.sln
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UACloudLibraryServer.UnitTe
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UACloudLibraryServer", "UACloudLibraryServer", "{0E5E2CB6-1971-8B53-CA5A-149BDFC2EEC3}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "db", "db", "{E88ED691-1F3F-46F6-9592-F910CE4D7325}"
ProjectSection(SolutionItems) = preProject
db\backup_db.ps1 = db\backup_db.ps1
db\initialize_db.ps1 = db\initialize_db.ps1
db\restore_db.ps1 = db\restore_db.ps1
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down
20 changes: 5 additions & 15 deletions UACloudLibraryServer/AppDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,26 +83,16 @@ public static string CreateConnectionString(IConfiguration configuration)

if (string.IsNullOrEmpty(connectionString))
{
connectionString = CreateConnectionStringFromEnvironment();
throw new InvalidOperationException(
"Connection string 'CloudLibraryPostgreSQL' is not configured. " +
"Set it via the configuration key 'ConnectionStrings:CloudLibraryPostgreSQL' " +
"(e.g. in appsettings.json) or the environment variable 'ConnectionStrings__CloudLibraryPostgreSQL'. " +
"Example value: 'Host=localhost;Port=5432;Database=uacloudlib;Username=uacloudlibrary-app;Password=<password>;Ssl Mode=Prefer;'.");
}

return connectionString;
}

private static string CreateConnectionStringFromEnvironment()
{
// Obtain connection string information from the environment
string Host = Environment.GetEnvironmentVariable("PostgreSQLEndpoint");
string User = Environment.GetEnvironmentVariable("PostgreSQLUsername");
string Password = Environment.GetEnvironmentVariable("PostgreSQLPassword");

string DBname = "uacloudlib";
string Port = "5432";

// Build connection string using parameters from portal
return $"Server={Host};Username={User};Database={DBname};Port={Port};Password={Password};SSLMode=Prefer";
}

public DbSet<NamespaceMetaDataModel> NamespaceMetaDataWithUnapproved { get; set; }

public DbSet<NodeSetModel> NodeSetsWithUnapproved { get; set; }
Expand Down
15 changes: 13 additions & 2 deletions UACloudLibraryServer/Authentication/UserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,18 @@ public UserService(UserManager<IdentityUser> userManager, ILoggerFactory logger,
public async Task<IEnumerable<Claim>> ValidateCredentialsAsync(string username, string password)
{
// check for admin
if (username.Equals("admin", StringComparison.OrdinalIgnoreCase))
string adminUsername = Environment.GetEnvironmentVariable("ServiceUsername");
if (string.IsNullOrEmpty(adminUsername))
{
adminUsername = _config.GetValue<string>("ServiceUsername");
}

if (string.IsNullOrEmpty(adminUsername))
{
adminUsername = "admin";
}

if (username.Equals(adminUsername, StringComparison.Ordinal))
{
string passwordFromEnvironment = Environment.GetEnvironmentVariable("ServicePassword");
if (string.IsNullOrEmpty(passwordFromEnvironment))
Expand All @@ -70,7 +81,7 @@ public async Task<IEnumerable<Claim>> ValidateCredentialsAsync(string username,
return null;
}

if (!password.Equals(passwordFromEnvironment, StringComparison.OrdinalIgnoreCase))
if (!password.Equals(passwordFromEnvironment, StringComparison.Ordinal))
{
return null;
}
Expand Down
Loading
Loading