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 = <