diff --git a/Adaptors/MongoDB/src/ServiceCollectionExt.cs b/Adaptors/MongoDB/src/ServiceCollectionExt.cs
index 9fa9e90a7..22491ef57 100644
--- a/Adaptors/MongoDB/src/ServiceCollectionExt.cs
+++ b/Adaptors/MongoDB/src/ServiceCollectionExt.cs
@@ -16,6 +16,8 @@
// along with this program. If not, see .
using System;
+using System.IO;
+using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using ArmoniK.Api.Common.Utils;
@@ -40,6 +42,8 @@
using MongoDB.Driver.Core.Configuration;
using MongoDB.Driver.Core.Extensions.DiagnosticSources;
+using static ArmoniK.Core.Utils.CertificateValidator;
+
namespace ArmoniK.Core.Adapters.MongoDB;
public static class ServiceCollectionExt
@@ -105,7 +109,6 @@ public static IServiceCollection AddMongoClient(this IServiceCollection services
services.AddOption(configuration,
Options.MongoDB.SettingSection,
out mongoOptions);
-
using var _ = logger.BeginNamedScope("MongoDB configuration",
("host", mongoOptions.Host),
("port", mongoOptions.Port));
@@ -140,30 +143,6 @@ public static IServiceCollection AddMongoClient(this IServiceCollection services
logger.LogTrace("No credentials provided");
}
- if (!string.IsNullOrEmpty(mongoOptions.CAFile))
- {
- var localTrustStore = new X509Store(StoreName.Root);
- var certificateCollection = new X509Certificate2Collection();
- try
- {
- certificateCollection.ImportFromPemFile(mongoOptions.CAFile);
- localTrustStore.Open(OpenFlags.ReadWrite);
- localTrustStore.AddRange(certificateCollection);
- logger.LogTrace("Imported mongodb certificate from file {path}",
- mongoOptions.CAFile);
- }
- catch (Exception ex)
- {
- logger.LogError("Root certificate import failed: {error}",
- ex.Message);
- throw;
- }
- finally
- {
- localTrustStore.Close();
- }
- }
-
string connectionString;
if (string.IsNullOrEmpty(mongoOptions.User) || string.IsNullOrEmpty(mongoOptions.Password))
{
@@ -190,6 +169,8 @@ public static IServiceCollection AddMongoClient(this IServiceCollection services
}
var settings = MongoClientSettings.FromUrl(new MongoUrl(connectionString));
+
+ // Configure the connection settings
settings.AllowInsecureTls = mongoOptions.AllowInsecureTls;
settings.UseTls = mongoOptions.Tls;
settings.DirectConnection = mongoOptions.DirectConnection;
@@ -197,6 +178,28 @@ public static IServiceCollection AddMongoClient(this IServiceCollection services
settings.MaxConnectionPoolSize = mongoOptions.MaxConnectionPoolSize;
settings.ServerSelectionTimeout = mongoOptions.ServerSelectionTimeout;
settings.ReplicaSetName = mongoOptions.ReplicaSet;
+
+ if (!string.IsNullOrEmpty(mongoOptions.CAFile))
+ {
+ if (!File.Exists(mongoOptions.CAFile))
+ {
+ logger.LogError("CA certificate Mongo file not found at {path}",
+ mongoOptions.CAFile);
+ throw new FileNotFoundException("CA certificate Mongo file not found",
+ mongoOptions.CAFile);
+ }
+
+ var (validationCallback, authority) = CertificateValidatorFactory.CreateCallback(mongoOptions.CAFile,
+ logger);
+
+ settings.SslSettings = new SslSettings
+ {
+ ClientCertificates = new X509Certificate2Collection(authority),
+ EnabledSslProtocols = SslProtocols.Tls12,
+ ServerCertificateValidationCallback = validationCallback,
+ };
+ }
+
settings.ClusterConfigurator = cb =>
{
//cb.Subscribe(e => logger.LogTrace("{CommandName} - {Command}",
@@ -205,6 +208,7 @@ public static IServiceCollection AddMongoClient(this IServiceCollection services
cb.Subscribe(new DiagnosticsActivityEventSubscriber());
};
+
var client = new MongoClient(settings);
services.AddSingleton(client);
diff --git a/Utils/src/ServerCertificateValidator.cs b/Utils/src/ServerCertificateValidator.cs
new file mode 100644
index 000000000..fccbbf374
--- /dev/null
+++ b/Utils/src/ServerCertificateValidator.cs
@@ -0,0 +1,124 @@
+// This file is part of the ArmoniK project
+//
+// Copyright (C) ANEO, 2021-2024. All rights reserved.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY, without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+using System.IO;
+using System.Linq;
+using System.Net.Security;
+using System.Security.Cryptography.X509Certificates;
+
+using Microsoft.Extensions.Logging;
+
+namespace ArmoniK.Core.Utils;
+
+public static class CertificateValidator
+{
+ public static RemoteCertificateValidationCallback ValidationCallback(ILogger logger,
+ X509Certificate2 authority)
+ => (sender,
+ certificate,
+ chain,
+ sslPolicyErrors) =>
+ {
+ if (certificate == null || chain == null)
+ {
+ logger.LogWarning("Certificate or certificate chain is null");
+ return false;
+ }
+
+ return ValidateServerCertificate(sender,
+ certificate,
+ chain,
+ sslPolicyErrors,
+ authority,
+ logger);
+ };
+
+
+ public static bool ValidateServerCertificate(object sender,
+ X509Certificate certificate,
+ X509Chain certChain,
+ SslPolicyErrors sslPolicyErrors,
+ X509Certificate2 authority,
+ ILogger logger)
+ {
+ // If there is any error other than untrusted root or partial chain, fail the validation
+ if ((sslPolicyErrors & ~SslPolicyErrors.RemoteCertificateChainErrors) != 0)
+ {
+ logger.LogDebug("SSL validation failed with errors: {sslPolicyErrors}",
+ sslPolicyErrors);
+ return false;
+ }
+
+ if (certificate == null)
+ {
+ logger.LogDebug("Certificate is null!");
+ return false;
+ }
+
+ if (certChain == null)
+ {
+ logger.LogDebug("Certificate chain is null!");
+ return false;
+ }
+
+ // If there is any error other than untrusted root or partial chain, fail the validation
+ if (certChain.ChainStatus.Any(status => status.Status is not X509ChainStatusFlags.UntrustedRoot and not X509ChainStatusFlags.PartialChain))
+ {
+ logger.LogDebug("SSL validation failed with chain status: {chainStatus}",
+ certChain.ChainStatus);
+ return false;
+ }
+
+ var cert = new X509Certificate2(certificate);
+ certChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
+ certChain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
+
+ certChain.ChainPolicy.ExtraStore.Add(authority);
+ if (!certChain.Build(cert))
+ {
+ return false;
+ }
+
+ var isTrusted = certChain.ChainElements.Any(x => x.Certificate.Thumbprint == authority.Thumbprint);
+ if (isTrusted)
+ {
+ logger.LogInformation("SSL validation succeeded");
+ }
+ else
+ {
+ logger.LogInformation("SSL validation failed with errors: {sslPolicyErrors}",
+ sslPolicyErrors);
+ }
+
+ return isTrusted;
+ }
+
+ public static class CertificateValidatorFactory
+ {
+ public static (RemoteCertificateValidationCallback, X509Certificate2) CreateCallback(string caFilePath,
+ ILogger logger)
+ {
+ var content = File.ReadAllText(caFilePath);
+ var authority = X509Certificate2.CreateFromPem(content);
+ logger.LogInformation("Loaded CA certificate from file {path}",
+ caFilePath);
+ var callback = ValidationCallback(logger,
+ authority);
+ return (callback, authority);
+ }
+ }
+}
diff --git a/terraform/main.tf b/terraform/main.tf
index 56f9b4b2b..0a826ea8a 100644
--- a/terraform/main.tf
+++ b/terraform/main.tf
@@ -115,6 +115,7 @@ module "submitter" {
generated_env_vars = local.environment
log_driver = module.fluenbit.log_driver
volumes = local.volumes
+ mounts = module.database.core_mounts
}
module "compute_plane" {
@@ -129,6 +130,7 @@ module "compute_plane" {
volumes = local.volumes
network = docker_network.armonik.id
log_driver = module.fluenbit.log_driver
+ mounts = module.database.core_mounts
}
module "metrics_exporter" {
@@ -138,6 +140,7 @@ module "metrics_exporter" {
network = docker_network.armonik.id
generated_env_vars = local.environment
log_driver = module.fluenbit.log_driver
+ mounts = module.database.core_mounts
}
module "partition_metrics_exporter" {
@@ -148,6 +151,7 @@ module "partition_metrics_exporter" {
generated_env_vars = local.environment
metrics_env_vars = module.metrics_exporter.metrics_env_vars
log_driver = module.fluenbit.log_driver
+ mounts = module.database.core_mounts
}
module "ingress" {
diff --git a/terraform/modules/compute_plane/inputs.tf b/terraform/modules/compute_plane/inputs.tf
index d599e9b0e..eba3a2bda 100644
--- a/terraform/modules/compute_plane/inputs.tf
+++ b/terraform/modules/compute_plane/inputs.tf
@@ -32,6 +32,10 @@ variable "volumes" {
type = map(string)
}
+variable "mounts" {
+ type = map(string)
+}
+
variable "replica_counter" {
type = number
}
diff --git a/terraform/modules/compute_plane/main.tf b/terraform/modules/compute_plane/main.tf
index 399f23190..b6202b1b6 100644
--- a/terraform/modules/compute_plane/main.tf
+++ b/terraform/modules/compute_plane/main.tf
@@ -74,5 +74,13 @@ resource "docker_container" "polling_agent" {
}
}
+ dynamic "upload" {
+ for_each = var.mounts
+ content {
+ source = upload.value
+ file = upload.key
+ }
+ }
+
depends_on = [docker_container.worker]
}
diff --git a/terraform/modules/monitoring/metrics/inputs.tf b/terraform/modules/monitoring/metrics/inputs.tf
index 3f7d806aa..3dff6611d 100644
--- a/terraform/modules/monitoring/metrics/inputs.tf
+++ b/terraform/modules/monitoring/metrics/inputs.tf
@@ -14,6 +14,10 @@ variable "generated_env_vars" {
type = map(string)
}
+variable "mounts" {
+ type = map(string)
+}
+
variable "exposed_port" {
type = number
default = 5002
diff --git a/terraform/modules/monitoring/metrics/main.tf b/terraform/modules/monitoring/metrics/main.tf
index 531674fbb..56e2e2b73 100644
--- a/terraform/modules/monitoring/metrics/main.tf
+++ b/terraform/modules/monitoring/metrics/main.tf
@@ -20,4 +20,12 @@ resource "docker_container" "metrics" {
internal = 1080
external = var.exposed_port
}
+
+ dynamic "upload" {
+ for_each = var.mounts
+ content {
+ source = upload.value
+ file = upload.key
+ }
+ }
}
diff --git a/terraform/modules/monitoring/partition_metrics/inputs.tf b/terraform/modules/monitoring/partition_metrics/inputs.tf
index 3fbf4c6a3..9724d39d5 100644
--- a/terraform/modules/monitoring/partition_metrics/inputs.tf
+++ b/terraform/modules/monitoring/partition_metrics/inputs.tf
@@ -19,6 +19,10 @@ variable "generated_env_vars" {
type = map(string)
}
+variable "mounts" {
+ type = map(string)
+}
+
variable "metrics_env_vars" {
type = map(string)
}
diff --git a/terraform/modules/monitoring/partition_metrics/main.tf b/terraform/modules/monitoring/partition_metrics/main.tf
index 87d3fede5..b75542fcf 100644
--- a/terraform/modules/monitoring/partition_metrics/main.tf
+++ b/terraform/modules/monitoring/partition_metrics/main.tf
@@ -20,4 +20,12 @@ resource "docker_container" "partition_metrics" {
internal = 1080
external = var.exposed_port
}
+
+ dynamic "upload" {
+ for_each = var.mounts
+ content {
+ source = upload.value
+ file = upload.key
+ }
+ }
}
diff --git a/terraform/modules/storage/database/mongo/certificates.tf b/terraform/modules/storage/database/mongo/certificates.tf
new file mode 100644
index 000000000..cce6c116f
--- /dev/null
+++ b/terraform/modules/storage/database/mongo/certificates.tf
@@ -0,0 +1,69 @@
+#------------------------------------------------------------------------------
+# Certificate Authority
+#------------------------------------------------------------------------------
+resource "tls_private_key" "root_mongodb" {
+ algorithm = "RSA"
+ ecdsa_curve = "P384"
+ rsa_bits = "4096"
+}
+
+resource "tls_self_signed_cert" "root_mongodb" {
+ private_key_pem = tls_private_key.root_mongodb.private_key_pem
+ is_ca_certificate = true
+ validity_period_hours = 100000
+ allowed_uses = [
+ "cert_signing",
+ "key_encipherment",
+ "digital_signature"
+ ]
+ subject {
+ organization = "ArmoniK mongodb Root (NonTrusted)"
+ common_name = "ArmoniK mongodb Root (NonTrusted) Private Certificate Authority"
+ country = "France"
+ }
+}
+
+#------------------------------------------------------------------------------
+# Certificate
+#------------------------------------------------------------------------------
+resource "tls_private_key" "mongodb_private_key" {
+ algorithm = "RSA"
+ ecdsa_curve = "P384"
+ rsa_bits = "4096"
+}
+
+resource "tls_cert_request" "mongodb_cert_request" {
+ private_key_pem = tls_private_key.mongodb_private_key.private_key_pem
+ subject {
+ country = "France"
+ common_name = "127.0.0.1"
+ # organization = "127.0.0.1"
+ }
+ ip_addresses = ["127.0.0.1"]
+ dns_names = [var.mongodb_params.database_name, "localhost"]
+}
+
+resource "tls_locally_signed_cert" "mongodb_certificate" {
+ cert_request_pem = tls_cert_request.mongodb_cert_request.cert_request_pem
+ ca_private_key_pem = tls_private_key.root_mongodb.private_key_pem
+ ca_cert_pem = tls_self_signed_cert.root_mongodb.cert_pem
+ validity_period_hours = 100000
+ allowed_uses = [
+ "key_encipherment",
+ "digital_signature",
+ "server_auth",
+ "client_auth",
+ "any_extended",
+ ]
+}
+
+locals {
+ server_key = format("%s\n%s", tls_locally_signed_cert.mongodb_certificate.cert_pem, tls_private_key.mongodb_private_key.private_key_pem)
+}
+
+resource "local_sensitive_file" "ca" {
+ content = tls_locally_signed_cert.mongodb_certificate.ca_cert_pem
+ filename = "${path.root}/generated/mongo/ca.pem"
+ file_permission = "0644"
+}
+
diff --git a/terraform/modules/storage/database/mongo/main.tf b/terraform/modules/storage/database/mongo/main.tf
index 388b9ca2d..f164cb68e 100644
--- a/terraform/modules/storage/database/mongo/main.tf
+++ b/terraform/modules/storage/database/mongo/main.tf
@@ -7,7 +7,7 @@ resource "docker_container" "database" {
name = var.mongodb_params.database_name
image = docker_image.database.image_id
- command = ["mongod", "--bind_ip_all", "--replSet", var.mongodb_params.replica_set_name]
+ command = ["mongod", "--bind_ip_all", "--replSet", var.mongodb_params.replica_set_name, "--tlsMode=requireTLS", "--tlsDisabledProtocols=TLS1_0", "--tlsCertificateKeyFile=/mongo-certificate/key.pem", "--tlsCAFile=/mongo-certificate/ca.pem", "--tlsAllowConnectionsWithoutCertificates"]
networks_advanced {
name = var.network
@@ -23,37 +23,62 @@ resource "docker_container" "database" {
dynamic "healthcheck" {
for_each = var.mongodb_params.windows ? [] : [1]
content {
- test = ["CMD", "mongosh", "--quiet", "--eval", "db.runCommand('ping').ok"]
+ test = ["CMD", "mongosh", "--quiet", "--tls", "--tlsCAFile", "/mongo-certificate/ca.pem", "--eval", "db.runCommand('ping').ok"]
interval = "3s"
retries = "2"
- timeout = "2s"
+ timeout = "3s"
}
}
-}
+ upload {
+ file = "/mongo-certificate/key.pem"
+ content = local.server_key
+ }
+ upload {
+ file = "/mongo-init.js"
+ content = local.mongo_init_repset
+ }
+
+ upload {
+ file = "/mongo-certificate/ca.pem"
+ content = tls_locally_signed_cert.mongodb_certificate.ca_cert_pem
+ }
+}
resource "time_sleep" "wait" {
- create_duration = var.mongodb_params.windows ? "15s" : "0s"
+ create_duration = var.mongodb_params.windows ? "30s" : "0s"
depends_on = [docker_container.database]
}
-
locals {
- linux_run = "docker run --net ${var.network} ${docker_image.database.image_id} mongosh mongodb://${docker_container.database.name}:27017/${var.mongodb_params.database_name}"
+ linux_run = "docker exec ${docker_container.database.name} mongosh mongodb://127.0.0.1:27017/${var.mongodb_params.database_name} --tls --tlsCAFile /mongo-certificate/ca.pem"
// mongosh is not installed in windows docker images so we need it to be installed locally
- windows_run = "mongosh.exe mongodb://localhost:${var.mongodb_params.exposed_port}/${var.mongodb_params.database_name}"
- prefix_run = var.mongodb_params.windows ? local.windows_run : local.linux_run
+ windows_run = "mongosh.exe mongodb://127.0.0.1:${var.mongodb_params.exposed_port}/${var.mongodb_params.database_name} --tls --tlsCAFile ${local_sensitive_file.ca.filename}"
+ prefix_run = var.mongodb_params.windows ? local.windows_run : local.linux_run
+ mongo_init_repset = <