(feature): sever side agent implementation

This commit is contained in:
Maksym Sadovnychyy 2024-06-05 21:54:10 +02:00
parent 5ecc436ddf
commit 6328afd5fb
30 changed files with 1084 additions and 190 deletions

View File

@ -122,4 +122,64 @@ frontend web
#---------------------------------------------------------------------
backend acme_challenge_backend
server acme_challenge 127.0.0.1:8080
```
```
## MaksIT agent
```bash
openssl rand -base64 32
```
```bash
sudo rpm -Uvh https://packages.microsoft.com/config/centos/8/packages-microsoft-prod.rpm
sudo dnf install -y dotnet-sdk-8.0
```
Copy sources to
```bash
sudo mkdir -p /opt/maks-it-agent
```
```bash
dotnet build --configuration Release
dotnet publish -c Release -o /opt/maks-it-agent
```
```bash
sudo nano /etc/systemd/system/maks-it-agent.service
```
```bash
[Unit]
Description=Maks-IT Agent
After=network.target
[Service]
WorkingDirectory=/opt/maks-it-agent
ExecStart=/usr/bin/dotnet /opt/maks-it-agent/Agent.dll --urls "http://*:5000"
Restart=always
# Restart service after 10 seconds if the dotnet service crashes:
RestartSec=10
KillSignal=SIGINT
SyslogIdentifier=dotnet-servicereloader
User=root
Environment=ASPNETCORE_ENVIRONMENT=Production
[Install]
WantedBy=multi-user.target
```
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now maks-it-agent.service
sudo systemctl status maks-it-agent.service
```

23
src/Agent/Agent.csproj Normal file
View File

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Models\Models.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="build_and_deploy.sh">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@ -0,0 +1,6 @@
namespace MaksIT.Agent {
public class Configuration {
public required string ApiKey { get; set; }
public required string CertsPath { get; set; }
}
}

View File

@ -0,0 +1,41 @@

using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using MaksIT.Models.Agent.Requests;
namespace MaksIT.Agent.Controllers;
[ApiController]
[Route("[controller]")]
public class CertsController : ControllerBase {
private readonly Configuration _appSettings;
public CertsController(
IOptions<Configuration> appSettings
) {
_appSettings = appSettings.Value;
}
[HttpPost("[action]")]
public IActionResult Upload([FromBody] CertsUploadRequest requestData) {
if (!Request.Headers.TryGetValue("X-API-KEY", out var extractedApiKey)) {
return Unauthorized("API Key is missing");
}
if (!_appSettings.ApiKey.Equals(extractedApiKey)) {
return Unauthorized("Unauthorized client");
}
foreach (var (fileName, fileContent) in requestData.Certs) {
System.IO.File.WriteAllText(Path.Combine(_appSettings.CertsPath, fileName), fileContent);
}
return Ok("Certificates uploaded successfully");
}
}

View File

@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Mvc;
namespace Agent.Controllers {
[ApiController]
[Route("[controller]")]
public class HelloWorldController : ControllerBase {
[HttpGet]
public IActionResult Get() {
return Ok("Hello, World!");
}
}
}

View File

@ -0,0 +1,61 @@
using System.Diagnostics;
using MaksIT.Models.Agent.Requests;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace MaksIT.Agent.Controllers;
[ApiController]
[Route("[controller]")]
public class ServiceController : ControllerBase {
private readonly Configuration _appSettings;
public ServiceController(
IOptions<Configuration> appSettings
) {
_appSettings = appSettings.Value;
}
[HttpPost("[action]")]
public IActionResult Reload([FromBody] ServiceReloadRequest requestData) {
var serviceName = requestData.ServiceName;
if (!Request.Headers.TryGetValue("X-API-KEY", out var extractedApiKey)) {
return Unauthorized("API Key is missing");
}
if (!_appSettings.ApiKey.Equals(extractedApiKey)) {
return Unauthorized("Unauthorized client");
}
try {
var processStartInfo = new ProcessStartInfo {
FileName = "/bin/systemctl",
Arguments = $"reload {serviceName}",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using (var process = new Process { StartInfo = processStartInfo }) {
process.Start();
process.WaitForExit();
var output = process.StandardOutput.ReadToEnd();
var error = process.StandardError.ReadToEnd();
if (process.ExitCode != 0) {
return StatusCode(500, $"Error reloading service: {error}");
}
return Ok($"Service {serviceName} reloaded successfully: {output}");
}
}
catch (Exception ex) {
return StatusCode(500, $"Exception: {ex.Message}");
}
}
}

34
src/Agent/Program.cs Normal file
View File

@ -0,0 +1,34 @@
using MaksIT.Agent;
var builder = WebApplication.CreateBuilder(args);
// Extract configuration
var configuration = builder.Configuration;
// Configure strongly typed settings objects
var configurationSection = configuration.GetSection("Configuration");
var appSettings = configurationSection.Get<Configuration>() ?? throw new ArgumentNullException();
// Allow configurations to be available through IOptions<Configuration>
builder.Services.Configure<Configuration>(configurationSection);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment()) {
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseAuthorization();
app.MapControllers();
app.Run();

View File

@ -0,0 +1,31 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:7748",
"sslPort": 0
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,6 @@
@ServiceReloader_HostAddress = http://localhost:5186
GET {{ServiceReloader_HostAddress}}/weatherforecast/
Accept: application/json
###

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -0,0 +1,14 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Configuration": {
"ApiKey": "UGnCaElLLJClHgUeet/yr7vNvPf13b1WkDJQMfsiP6I=",
"CertsPath": "/etc/haproxy/certs"
}
}

View File

@ -0,0 +1,77 @@
#!/bin/bash
# Variables
SERVICE_NAME="maks-it-agent"
SERVICE_PORT="5000"
SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service"
INSTALL_DIR="/opt/$SERVICE_NAME"
DOTNET_EXEC="/usr/bin/dotnet"
EXEC_CMD="$DOTNET_EXEC $INSTALL_DIR/Agent.dll --urls \"http://*:$SERVICE_PORT\""
APPSETTINGS_FILE="appsettings.json"
NO_NEW_KEY_FLAG="--no-new-key"
# Update package index and install the Microsoft package repository
sudo rpm -Uvh https://packages.microsoft.com/config/centos/8/packages-microsoft-prod.rpm
sudo dnf install -y dotnet-sdk-8.0
# Check if the service exists and stop it if it does
if systemctl list-units --full -all | grep -Fq "$SERVICE_NAME.service"; then
sudo systemctl stop $SERVICE_NAME.service
sudo systemctl disable $SERVICE_NAME.service
sudo rm -f $SERVICE_FILE
fi
# Clean up the old files if they exist
sudo rm -rf $INSTALL_DIR
# Create the application directory
sudo mkdir -p $INSTALL_DIR
# Update appsettings.json if --no-new-key flag is not provided
if [[ "$1" != "$NO_NEW_KEY_FLAG" ]]; then
NEW_API_KEY=$(openssl rand -base64 32)
jq --arg newApiKey "$NEW_API_KEY" '.Configuration.ApiKey = $newApiKey' $APPSETTINGS_FILE > tmp.$$.json && mv tmp.$$.json $APPSETTINGS_FILE
fi
# Build and publish the .NET application
sudo dotnet build --configuration Release
sudo dotnet publish -c Release -o $INSTALL_DIR
# Create the systemd service unit file
sudo bash -c "cat > $SERVICE_FILE <<EOL
[Unit]
Description=Maks-IT Agent
After=network.target
[Service]
WorkingDirectory=$INSTALL_DIR
ExecStart=$EXEC_CMD
Restart=always
RestartSec=10
KillSignal=SIGINT
SyslogIdentifier=dotnet-servicereloader
User=root
Environment=ASPNETCORE_ENVIRONMENT=Production
[Install]
WantedBy=multi-user.target
EOL"
# Reload systemd to recognize the new service, enable it to start on boot, and start the service now
sudo systemctl daemon-reload
sudo systemctl enable --now $SERVICE_NAME.service
# Create the firewall service rule
echo '<?xml version="1.0" encoding="utf-8"?>
<service>
<short>Maks-IT Agent</short>
<port protocol="tcp" port="'$SERVICE_PORT'"/>
</service>' > /etc/firewalld/services/maks-it-agent.xml
sleep 10
# Add the services to the firewall
firewall-cmd --permanent --add-service=maks-it-agent
# Reload the firewall
firewall-cmd --reload

View File

@ -15,10 +15,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{3374FDB1
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SSHProviderTests", "Tests\SSHSerivceTests\SSHProviderTests.csproj", "{3937760A-FFB3-4A8C-ABD1-CDDCE1D977C4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LetsEncryptServer", "LetsEncryptServer\LetsEncryptServer.csproj", "{B5F39E04-C2E3-49BF-82C2-9DEBAA949E3D}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LetsEncryptServer", "LetsEncryptServer\LetsEncryptServer.csproj", "{B5F39E04-C2E3-49BF-82C2-9DEBAA949E3D}"
EndProject
Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{0233E43F-435D-4309-B20C-ECD4BFBD2E63}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Agent", "Agent\Agent.csproj", "{871BDED3-C6AE-437D-9B45-3AA3F184D002}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Models", "Models\Models.csproj", "{6814169B-D4D0-40B2-9FA9-89997DD44C30}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -53,6 +57,14 @@ Global
{0233E43F-435D-4309-B20C-ECD4BFBD2E63}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0233E43F-435D-4309-B20C-ECD4BFBD2E63}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0233E43F-435D-4309-B20C-ECD4BFBD2E63}.Release|Any CPU.Build.0 = Release|Any CPU
{871BDED3-C6AE-437D-9B45-3AA3F184D002}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{871BDED3-C6AE-437D-9B45-3AA3F184D002}.Debug|Any CPU.Build.0 = Debug|Any CPU
{871BDED3-C6AE-437D-9B45-3AA3F184D002}.Release|Any CPU.ActiveCfg = Release|Any CPU
{871BDED3-C6AE-437D-9B45-3AA3F184D002}.Release|Any CPU.Build.0 = Release|Any CPU
{6814169B-D4D0-40B2-9FA9-89997DD44C30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6814169B-D4D0-40B2-9FA9-89997DD44C30}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6814169B-D4D0-40B2-9FA9-89997DD44C30}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6814169B-D4D0-40B2-9FA9-89997DD44C30}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -1,20 +1,17 @@
namespace MaksIT.LetsEncryptServer {
public class Server {
public required string Ip { get; set; }
public required int SocketPort { get; set; }
public required int SSHPort { get; set; }
public required string Path { get; set; }
public class Agent {
public required string AgentHostname { get; set; }
public required int AgentPort { get; set; }
public required string AgentKey { get; set; }
public required string Username { get; set; }
public string? Password { get; set; }
public string[]? PrivateKeys { get; set; }
public required string ServiceToReload { get; set; }
}
public class Configuration {
public required string Production { get; set; }
public required string Staging { get; set; }
public required bool DevMode { get; set; }
public required Server Server { get; set; }
public required Agent Agent { get; set; }
}
}

View File

@ -3,8 +3,8 @@ using Microsoft.Extensions.Options;
using DomainResults.Mvc;
using MaksIT.LetsEncryptServer.Models.Requests;
using MaksIT.LetsEncryptServer.Services;
using MaksIT.Models.LetsEncryptServer.Requests;
namespace MaksIT.LetsEncryptServer.Controllers;
@ -107,8 +107,8 @@ public class CertsFlowController : ControllerBase {
/// <param name="requestData"></param>
/// <returns></returns>
[HttpPost("[action]/{sessionId}")]
public IActionResult ApplyCertificates(Guid sessionId, [FromBody] GetCertificatesRequest requestData) {
var result = _certsFlowService.ApplyCertificates(sessionId, requestData);
public async Task<IActionResult> ApplyCertificates(Guid sessionId, [FromBody] GetCertificatesRequest requestData) {
var result = await _certsFlowService.ApplyCertificates(sessionId, requestData);
return result.ToActionResult();
}
}

View File

@ -17,11 +17,7 @@
<ItemGroup>
<ProjectReference Include="..\LetsEncrypt\LetsEncrypt.csproj" />
<ProjectReference Include="..\SSHProvider\SSHProvider.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Models\Responses\" />
<ProjectReference Include="..\Models\Models.csproj" />
</ItemGroup>
</Project>

View File

@ -28,6 +28,7 @@ builder.Services.AddMemoryCache();
builder.Services.AddHttpClient<ILetsEncryptService, LetsEncryptService>();
builder.Services.AddScoped<ICertsFlowService, CertsFlowService>();
builder.Services.AddSingleton<ICacheService, CacheService>();
builder.Services.AddHttpClient<IAgentService, AgentService>();
var app = builder.Build();

View File

@ -0,0 +1,72 @@
using DomainResults.Common;
using MaksIT.Models.Agent.Requests;
using Microsoft.Extensions.Options;
using System.Text;
using System.Text.Json;
namespace MaksIT.LetsEncryptServer.Services {
public interface IAgentService {
Task<IDomainResult> GetHelloWorld();
Task<IDomainResult> UploadCerts(Dictionary<string, string> certs);
Task<IDomainResult> ReloadService(string serviceName);
}
public class AgentService : IAgentService {
private readonly Configuration _appSettings;
private readonly ILogger<AgentService> _logger;
private readonly HttpClient _httpClient;
public AgentService(
IOptions<Configuration> appSettings,
ILogger<AgentService> logger,
HttpClient httpClient
) {
_appSettings = appSettings.Value;
_logger = logger;
_httpClient = httpClient;
}
public Task<IDomainResult> GetHelloWorld() {
throw new NotImplementedException();
}
public async Task<IDomainResult> ReloadService(string serviceName) {
var requestBody = new ServiceReloadRequest { ServiceName = serviceName };
var endpoint = $"/Service/Reload";
return await SendHttpRequest(requestBody, endpoint);
}
public async Task<IDomainResult> UploadCerts(Dictionary<string, string> certs) {
var requestBody = new CertsUploadRequest { Certs = certs };
var endpoint = $"/Certs/Upload";
return await SendHttpRequest(requestBody, endpoint);
}
private async Task<IDomainResult> SendHttpRequest<T>(T requestBody, string endpoint) {
try {
var request = new HttpRequestMessage(HttpMethod.Post, $"{_appSettings.Agent.AgentHostname}:{_appSettings.Agent.AgentPort}{endpoint}") {
Content = new StringContent(JsonSerializer.Serialize(requestBody), Encoding.UTF8, "application/json")
};
request.Headers.Add("x-api-key", _appSettings.Agent.AgentKey);
request.Headers.Add("accept", "application/json");
var response = await _httpClient.SendAsync(request);
if (response.IsSuccessStatusCode) {
return IDomainResult.Success();
}
else {
_logger.LogError($"Request to {endpoint} failed with status code: {response.StatusCode}");
return IDomainResult.Failed($"Request to {endpoint} failed with status code: {response.StatusCode}");
}
}
catch (Exception ex) {
_logger.LogError(ex, "Something went wrong");
return IDomainResult.Failed("Something went wrong");
}
}
}
}

View File

@ -6,8 +6,7 @@ using DomainResults.Common;
using MaksIT.LetsEncrypt.Entities;
using MaksIT.LetsEncrypt.Services;
using MaksIT.LetsEncryptServer.Models.Requests;
using MaksIT.SSHProvider;
using MaksIT.Models.LetsEncryptServer.Requests;
namespace MaksIT.LetsEncryptServer.Services;
@ -24,7 +23,7 @@ public interface ICertsFlowService : ICertsFlowServiceBase {
Task<IDomainResult> CompleteChallengesAsync(Guid sessionId);
Task<IDomainResult> GetOrderAsync(Guid sessionId, GetOrderRequest requestData);
Task<IDomainResult> GetCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData);
(Dictionary<string, string>?, IDomainResult) ApplyCertificates(Guid sessionId, GetCertificatesRequest requestData);
Task<(Dictionary<string, string>?, IDomainResult)> ApplyCertificates(Guid sessionId, GetCertificatesRequest requestData);
}
public class CertsFlowService : ICertsFlowService {
@ -33,6 +32,7 @@ public class CertsFlowService : ICertsFlowService {
private readonly ILogger<CertsFlowService> _logger;
private readonly ILetsEncryptService _letsEncryptService;
private readonly ICacheService _cacheService;
private readonly IAgentService _agentService;
private readonly string _acmePath;
@ -40,12 +40,14 @@ public class CertsFlowService : ICertsFlowService {
IOptions<Configuration> appSettings,
ILogger<CertsFlowService> logger,
ILetsEncryptService letsEncryptService,
ICacheService cashService
ICacheService cashService,
IAgentService agentService
) {
_appSettings = appSettings.Value;
_logger = logger;
_letsEncryptService = letsEncryptService;
_cacheService = cashService;
_agentService = agentService;
_acmePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "acme");
if (!Directory.Exists(_acmePath))
@ -138,7 +140,7 @@ public class CertsFlowService : ICertsFlowService {
return IDomainResult.Success();
}
public (Dictionary<string, string>?, IDomainResult) ApplyCertificates(Guid sessionId, GetCertificatesRequest requestData) {
public async Task<(Dictionary<string, string>?, IDomainResult)> ApplyCertificates(Guid sessionId, GetCertificatesRequest requestData) {
var results = new Dictionary<string, string>();
foreach (var subject in requestData.Hostnames) {
@ -150,16 +152,13 @@ public class CertsFlowService : ICertsFlowService {
results.Add(subject, content);
}
var uploadResult = UploadToServer(results);
if (!uploadResult.IsSuccess)
// TODO: send the certificates to the server
var uploadResult = await _agentService.UploadCerts(results);
if(!uploadResult.IsSuccess)
return (null, uploadResult);
//var notifyResult = NotifyHaproxy(results);
//if (!notifyResult.IsSuccess)
// return (null, notifyResult);
var reloadResult = ReloadServer();
if (!reloadResult.IsSuccess)
var reloadResult = await _agentService.ReloadService(_appSettings.Agent.ServiceToReload);
if(!reloadResult.IsSuccess)
return (null, reloadResult);
return IDomainResult.Success(results);
@ -175,129 +174,6 @@ public class CertsFlowService : ICertsFlowService {
return IDomainResult.Success(fileContent);
}
private IDomainResult UploadToServer(Dictionary<string, string> results) {
var server = _appSettings.Server;
try {
using (SSHService sshClient = (server.PrivateKeys != null && server.PrivateKeys.Any(x => !string.IsNullOrWhiteSpace(x)))
? new SSHService(_logger, server.Ip, server.SSHPort, server.Username, server.PrivateKeys)
: !string.IsNullOrWhiteSpace(server.Password)
? new SSHService(_logger, server.Ip, server.SSHPort, server.Username, server.Password)
: throw new ArgumentNullException("Neither private keys nor password was provided")) {
var sshConnectResult = sshClient.Connect();
if (!sshConnectResult.IsSuccess)
return sshConnectResult;
foreach (var result in results) {
var uploadResult = sshClient.Upload(server.Path, result.Key, Encoding.UTF8.GetBytes(result.Value));
if (!uploadResult.IsSuccess)
return uploadResult;
}
}
}
catch (Exception ex) {
var message = "Unable to upload files to remote server";
_logger.LogError(ex, message);
return IDomainResult.CriticalDependencyError(message);
}
return IDomainResult.Success();
}
private IDomainResult ReloadServer() {
var server = _appSettings.Server;
try {
using (SSHService sshClient = (server.PrivateKeys != null && server.PrivateKeys.Any(x => !string.IsNullOrWhiteSpace(x)))
? new SSHService(_logger, server.Ip, server.SSHPort, server.Username, server.PrivateKeys)
: !string.IsNullOrWhiteSpace(server.Password)
? new SSHService(_logger, server.Ip, server.SSHPort, server.Username, server.Password)
: throw new ArgumentNullException("Neither private keys nor password was provided")) {
var sshConnectResult = sshClient.Connect();
if (!sshConnectResult.IsSuccess)
return sshConnectResult;
// TODO: Prefer to create the native linux service which can receive the signal to reload the services
return sshClient.RunSudoCommand("", "systemctl reload haproxy");
}
}
catch (Exception ex) {
var message = "Unable to upload files to remote server";
_logger.LogError(ex, message);
return IDomainResult.CriticalDependencyError(message);
}
return IDomainResult.Success();
}
/// <summary>
/// Currently not working
/// </summary>
/// <param name="results"></param>
/// <returns></returns>
private IDomainResult NotifyHaproxy(Dictionary<string, string> results) {
var server = _appSettings.Server;
try {
using (var client = new TcpClient(server.Ip, server.SocketPort))
using (var networkStream = client.GetStream())
using (var writer = new StreamWriter(networkStream, Encoding.ASCII))
using (var reader = new StreamReader(networkStream, Encoding.ASCII)) {
writer.AutoFlush = true;
foreach (var result in results) {
var certFile = result.Key;
// Prepare the certificate
string prepareCommand = $"new ssl cert {server.Path}/{certFile}";
writer.WriteLine(prepareCommand);
writer.Flush();
string prepareResponse = reader.ReadLine();
//if (prepareResponse.Contains("error", StringComparison.OrdinalIgnoreCase)) {
// _logger.LogError($"Error while preparing certificate {certFile}: {prepareResponse}");
// return IDomainResult.CriticalDependencyError($"Error while preparing certificate {certFile}");
//}
// Set the certificate
string setCommand = $"set ssl cert {server.Path}/{certFile} <<\n{result.Value}\n";
writer.WriteLine(setCommand);
writer.Flush();
string setResponse = reader.ReadLine();
//if (setResponse.Contains("error", StringComparison.OrdinalIgnoreCase)) {
// _logger.LogError($"Error while setting certificate {certFile}: {setResponse}");
// return IDomainResult.CriticalDependencyError($"Error while setting certificate {certFile}");
//}
// Commit the certificate
string commitCommand = $"commit ssl cert {server.Path}/{certFile}";
writer.WriteLine(commitCommand);
writer.Flush();
string commitResponse = reader.ReadLine();
//if (commitResponse.Contains("error", StringComparison.OrdinalIgnoreCase)) {
// _logger.LogError($"Error while committing certificate {certFile}: {commitResponse}");
// return IDomainResult.CriticalDependencyError($"Error while committing certificate {certFile}");
//}
}
_logger.LogInformation("Certificates committed successfully.");
}
}
catch (Exception ex) {
var message = "An error occurred while committing certificates";
_logger.LogError(ex, message);
return IDomainResult.CriticalDependencyError(message);
}
return IDomainResult.Success();
}
private void DeleteExporedChallenges() {
var currentDate = DateTime.Now;

View File

@ -13,14 +13,12 @@
"DevMode": true,
"Server": {
"Ip": "192.168.1.4",
"SocketPort": 9999,
"SSHPort": 22,
"Path": "/etc/haproxy/certs",
"Username": "acme",
"PrivateKeys": [],
"Password": "acme"
"Agent": {
"AgentHostname": "http://lblsrv0001.corp.maks-it.com",
"AgentPort": 5000,
"AgentKey": "UGnCaElLLJClHgUeet/yr7vNvPf13b1WkDJQMfsiP6I=",
"ServiceToReload": "haproxy"
}
}
}

View File

@ -0,0 +1,7 @@
namespace MaksIT.Models.Agent.Requests {
public class CertsUploadRequest {
public Dictionary<string, string> Certs { get; set; }
}
}

View File

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MaksIT.Models.Agent.Requests {
public class ServiceReloadRequest {
public string ServiceName { get; set; }
}
}

View File

@ -1,4 +1,4 @@
namespace MaksIT.LetsEncryptServer.Models.Requests {
namespace MaksIT.Models.LetsEncryptServer.Requests {
public class GetCertificatesRequest {
public string[] Hostnames { get; set; }
}

View File

@ -1,4 +1,4 @@
namespace MaksIT.LetsEncryptServer.Models.Requests {
namespace MaksIT.Models.LetsEncryptServer.Requests {
public class GetOrderRequest {
public string[] Hostnames { get; set; }
}

View File

@ -4,7 +4,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MaksIT.LetsEncryptServer.Models.Requests {
namespace MaksIT.Models.LetsEncryptServer.Requests {
public class InitRequest {
public string[] Contacts { get; set; }
}

View File

@ -1,4 +1,4 @@
namespace MaksIT.LetsEncryptServer.Models.Requests {
namespace MaksIT.Models.LetsEncryptServer.Requests {
public class NewOrderRequest {
public string[] Hostnames { get; set; }

14
src/Models/Models.csproj Normal file
View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Folder Include="Agent\Responses\" />
<Folder Include="LetsEncryptServer\Responses\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,479 @@
{
"info": {
"_postman_id": "728f64b6-893b-43fa-802e-ee836d1dc372",
"name": "LetsEncrypt Production",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "33635244"
},
"item": [
{
"name": "letsencrypt production",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "https://acme-v02.api.letsencrypt.org/directory",
"protocol": "https",
"host": [
"acme-v02",
"api",
"letsencrypt",
"org"
],
"path": [
"directory"
]
}
},
"response": []
},
{
"name": "configure client",
"event": [
{
"listen": "test",
"script": {
"exec": [
"// Ensure the response status code is 200 (OK)\r",
"if (pm.response.code === 200) {\r",
" // Get the plain text response\r",
" let responseBody = pm.response.text();\r",
" \r",
" // Remove the surrounding quotes if present\r",
" responseBody = responseBody.replace(/^\"|\"$/g, '');\r",
" \r",
" // Check if the response body is a valid GUID\r",
" if (/^[0-9a-fA-F-]{36}$/.test(responseBody)) {\r",
" // Set the environment variable sessionId with the response\r",
" pm.environment.set(\"sessionId\", responseBody);\r",
" console.log(`sessionId set to: ${responseBody}`);\r",
" } else {\r",
" console.log(\"Response body is not a valid GUID\");\r",
" }\r",
"} else {\r",
" console.log(`Request failed with status code: ${pm.response.code}`);\r",
"}\r",
""
],
"type": "text/javascript",
"packages": {}
}
}
],
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Accept",
"value": "application/json"
}
],
"url": {
"raw": "http://localhost:8080/CertsFlow/ConfigureClient",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"CertsFlow",
"ConfigureClient"
]
}
},
"response": []
},
{
"name": "terms of service",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://localhost:8080/CertsFlow/TermsOfService/{{sessionId}}",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"CertsFlow",
"TermsOfService",
"{{sessionId}}"
]
}
},
"response": []
},
{
"name": "init",
"event": [
{
"listen": "test",
"script": {
"exec": [
"// Ensure the response status code is 200 (OK)\r",
"if (pm.response.code === 200) {\r",
" // Get the plain text response\r",
" let responseBody = pm.response.text();\r",
" \r",
" // Remove the surrounding quotes if present\r",
" responseBody = responseBody.replace(/^\"|\"$/g, '');\r",
" \r",
" // Check if the response body is a valid GUID\r",
" if (/^[0-9a-fA-F-]{36}$/.test(responseBody)) {\r",
" // Set the environment variable accountId with the response\r",
" pm.environment.set(\"accountId\", responseBody);\r",
" console.log(`accountId set to: ${responseBody}`);\r",
" } else {\r",
" console.log(\"Response body is not a valid GUID\");\r",
" }\r",
"} else {\r",
" console.log(`Request failed with status code: ${pm.response.code}`);\r",
"}\r",
""
],
"type": "text/javascript",
"packages": {}
}
},
{
"listen": "prerequest",
"script": {
"exec": [
"// Retrieve sessionId and accountId from environment variables or global variables\r",
"var sessionId = pm.environment.get(\"sessionId\") || pm.globals.get(\"sessionId\");\r",
"var accountId = pm.environment.get(\"accountId\") || pm.globals.get(\"accountId\");\r",
"\r",
"// Base URL without the optional accountId parameter\r",
"var baseUrl = `http://localhost:8080/CertsFlow/Init/${sessionId}`;\r",
"\r",
"// Append the accountId if it is provided\r",
"if (accountId) {\r",
" pm.request.url = `${baseUrl}/${accountId}`;\r",
"} else {\r",
" pm.request.url = baseUrl;\r",
"}"
],
"type": "text/javascript",
"packages": {}
}
}
],
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Accept",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\r\n \"contacts\": [\r\n \"maksym.sadovnychyy@gmail.com\"\r\n ]\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "http://localhost:8080/CertsFlow/Init/{{sessionId}}/{{accountId}}",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"CertsFlow",
"Init",
"{{sessionId}}",
"{{accountId}}"
]
}
},
"response": []
},
{
"name": "new order",
"event": [
{
"listen": "test",
"script": {
"exec": [
"// Ensure the response status code is 200 (OK)\r",
"if (pm.response.code === 200) {\r",
" // Parse the JSON response\r",
" let responseBody;\r",
" try {\r",
" responseBody = pm.response.json();\r",
" } catch (e) {\r",
" console.error(\"Failed to parse JSON response:\", e);\r",
" return;\r",
" }\r",
"\r",
" // Check if the response is an array and has at least one element\r",
" if (Array.isArray(responseBody) && responseBody.length > 0) {\r",
" // Get the first element of the array\r",
" const firstElement = responseBody[0];\r",
" \r",
" // Set the environment variable challenge with the first element\r",
" pm.environment.set(\"challenge\", firstElement);\r",
" console.log(`challenge set to: ${firstElement}`);\r",
" } else {\r",
" console.log(\"Response body is not an array or is empty\");\r",
" }\r",
"} else {\r",
" console.log(`Request failed with status code: ${pm.response.code}`);\r",
"}\r",
""
],
"type": "text/javascript",
"packages": {}
}
}
],
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Accept",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\r\n \"hostnames\": [\r\n \"maks-it.com\",\r\n \"auth.maks-it.com\"\r\n ],\r\n \"challengeType\": \"http-01\"\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "http://localhost:8080/CertsFlow/NewOrder/{{sessionId}}",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"CertsFlow",
"NewOrder",
"{{sessionId}}"
]
}
},
"response": []
},
{
"name": "acme-challenge local",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://localhost:8080/.well-known/acme-challenge/{{challenge}}",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
".well-known",
"acme-challenge",
"{{challenge}}"
]
}
},
"response": []
},
{
"name": "acme-challenge",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://maks-it.com/.well-known/acme-challenge/{{challenge}}",
"protocol": "http",
"host": [
"maks-it",
"com"
],
"path": [
".well-known",
"acme-challenge",
"{{challenge}}"
]
}
},
"response": []
},
{
"name": "complete challenges",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Accept",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "http://localhost:8080/CertsFlow/CompleteChallenges/{{sessionId}}",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"CertsFlow",
"CompleteChallenges",
"{{sessionId}}"
]
}
},
"response": []
},
{
"name": "get order",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Accept",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\r\n \"hostnames\": [\r\n \"maks-it.com\",\r\n \"auth.maks-it.com\"\r\n ]\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "http://localhost:8080/CertsFlow/GetOrder/{{sessionId}}",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"CertsFlow",
"GetOrder",
"{{sessionId}}"
]
}
},
"response": []
},
{
"name": "get certificates",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Accept",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\r\n \"hostnames\": [\r\n \"maks-it.com\",\r\n \"auth.maks-it.com\"\r\n ]\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "http://localhost:8080/CertsFlow/GetCertificates/{{sessionId}}",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"CertsFlow",
"GetCertificates",
"{{sessionId}}"
]
}
},
"response": []
},
{
"name": "apply certificates",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Accept",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\r\n \"hostnames\": [\r\n \"maks-it.com\",\r\n \"auth.maks-it.com\"\r\n ]\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "http://localhost:8080/CertsFlow/ApplyCertificates/{{sessionId}}",
"protocol": "http",
"host": [
"localhost"
],
"port": "8080",
"path": [
"CertsFlow",
"ApplyCertificates",
"{{sessionId}}"
]
}
},
"response": []
}
]
}

View File

@ -1,7 +1,7 @@
{
"info": {
"_postman_id": "728f64b6-893b-43fa-802e-ee836d1dc372",
"name": "LetsEncrypt",
"_postman_id": "95186b61-1197-4a6e-a90f-d97223528d90",
"name": "LetsEncrypt Staging",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "33635244"
},
@ -28,27 +28,6 @@
},
"response": []
},
{
"name": "letsencrypt production",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "https://acme-v02.api.letsencrypt.org/directory",
"protocol": "https",
"host": [
"acme-v02",
"api",
"letsencrypt",
"org"
],
"path": [
"directory"
]
}
},
"response": []
},
{
"name": "configure client",
"event": [

View File

@ -0,0 +1,77 @@
{
"info": {
"_postman_id": "1e13f461-ccaa-436a-92e4-e14c05131b96",
"name": "Maks-IT Agent",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "33635244"
},
"item": [
{
"name": "reload service",
"request": {
"method": "POST",
"header": [
{
"key": "x-api-key",
"value": "{{agentKey}}"
},
{
"key": "Accept",
"value": "application/json"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\r\n \"serviceName\": {{serviceName}}\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "http://lblsrv0001.corp.maks-it.com:5000/Service/Reload",
"protocol": "http",
"host": [
"lblsrv0001",
"corp",
"maks-it",
"com"
],
"port": "5000",
"path": [
"Service",
"Reload"
]
}
},
"response": []
},
{
"name": "hello world",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://lblsrv0001.corp.maks-it.com:5000/HelloWorld",
"protocol": "http",
"host": [
"lblsrv0001",
"corp",
"maks-it",
"com"
],
"port": "5000",
"path": [
"HelloWorld"
]
}
},
"response": []
}
]
}