Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

build: add tls for mongo in deployment #754

Draft
wants to merge 14 commits into
base: main
Choose a base branch
from
Draft
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
45 changes: 20 additions & 25 deletions Adaptors/MongoDB/src/ServiceCollectionExt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.

using System;
using System.IO;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;

using ArmoniK.Api.Common.Utils;
Expand All @@ -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
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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))
{
Expand All @@ -190,13 +169,28 @@ 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;
settings.Scheme = ConnectionStringScheme.MongoDB;
settings.MaxConnectionPoolSize = mongoOptions.MaxConnectionPoolSize;
settings.ServerSelectionTimeout = mongoOptions.ServerSelectionTimeout;
settings.ReplicaSetName = mongoOptions.ReplicaSet;

if (!string.IsNullOrEmpty(mongoOptions.CAFile))
{
var (validationCallback, authority) = CreateCallback(mongoOptions.CAFile,
logger);

settings.SslSettings = new SslSettings
{
EnabledSslProtocols = SslProtocols.Tls12,
ServerCertificateValidationCallback = validationCallback,
};
}

settings.ClusterConfigurator = cb =>
{
//cb.Subscribe<CommandStartedEvent>(e => logger.LogTrace("{CommandName} - {Command}",
Expand All @@ -205,6 +199,7 @@ public static IServiceCollection AddMongoClient(this IServiceCollection services
cb.Subscribe(new DiagnosticsActivityEventSubscriber());
};


var client = new MongoClient(settings);

services.AddSingleton<IMongoClient>(client);
Expand Down
130 changes: 130 additions & 0 deletions Utils/src/ServerCertificateValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// 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 <http://www.gnu.org/licenses/>.

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 (RemoteCertificateValidationCallback, X509Certificate2) CreateCallback(string caFilePath,
ILogger logger)
{
if (!File.Exists(caFilePath))
{
logger.LogError("CA certificate Mongo file not found at {path}",
caFilePath);
throw new FileNotFoundException("CA certificate Mongo file not found",
caFilePath);
}

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);
}
}
4 changes: 4 additions & 0 deletions terraform/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand All @@ -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" {
Expand All @@ -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" {
Expand All @@ -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" {
Expand Down
4 changes: 4 additions & 0 deletions terraform/modules/compute_plane/inputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ variable "volumes" {
type = map(string)
}

variable "mounts" {
type = map(string)
}

variable "replica_counter" {
type = number
}
Expand Down
8 changes: 8 additions & 0 deletions terraform/modules/compute_plane/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}
4 changes: 4 additions & 0 deletions terraform/modules/monitoring/metrics/inputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ variable "generated_env_vars" {
type = map(string)
}

variable "mounts" {
type = map(string)
}

variable "exposed_port" {
type = number
default = 5002
Expand Down
8 changes: 8 additions & 0 deletions terraform/modules/monitoring/metrics/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
4 changes: 4 additions & 0 deletions terraform/modules/monitoring/partition_metrics/inputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ variable "generated_env_vars" {
type = map(string)
}

variable "mounts" {
type = map(string)
}

variable "metrics_env_vars" {
type = map(string)
}
Expand Down
8 changes: 8 additions & 0 deletions terraform/modules/monitoring/partition_metrics/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
69 changes: 69 additions & 0 deletions terraform/modules/storage/database/mongo/certificates.tf
Original file line number Diff line number Diff line change
@@ -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"
}

Loading
Loading