diff --git a/LetsEncrypt.postman_collection.json b/LetsEncrypt.postman_collection.json index d92f62e..a678f23 100644 --- a/LetsEncrypt.postman_collection.json +++ b/LetsEncrypt.postman_collection.json @@ -49,48 +49,6 @@ }, "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": "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": "terms of service", "request": { @@ -318,7 +276,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"hostnames\": [\r\n \"maks-it.com\",\r\n \"www.maks-it.com\"\r\n ],\r\n \"challengeType\": \"http-01\"\r\n}", + "raw": "{\r\n \"hostnames\": [\r\n \"maks-it.com\"\r\n ],\r\n \"challengeType\": \"http-01\"\r\n}", "options": { "raw": { "language": "json" @@ -341,6 +299,48 @@ }, "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": { @@ -396,7 +396,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"hostnames\": [\r\n \"maks-it.com\",\r\n \"www.maks-it.com\"\r\n ]\r\n}", + "raw": "{\r\n \"hostnames\": [\r\n \"maks-it.com\"\r\n ]\r\n}", "options": { "raw": { "language": "json" @@ -435,7 +435,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"hostnames\": [\r\n \"maks-it.com\",\r\n \"www.maks-it.com\"\r\n ]\r\n}", + "raw": "{\r\n \"hostnames\": [\r\n \"maks-it.com\"\r\n ]\r\n}", "options": { "raw": { "language": "json" @@ -474,7 +474,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"hostnames\": [\r\n \"maks-it.com\",\r\n \"www.maks-it.com\"\r\n ]\r\n}", + "raw": "{\r\n \"hostnames\": [\r\n \"maks-it.com\"\r\n ]\r\n}", "options": { "raw": { "language": "json" diff --git a/README.md b/README.md index b286e52..ae7d6c7 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,23 @@ Simple client to obtain Let's Encrypt HTTPS certificates developed with .net cor ## Haproxy configuration +```bash +# Create the user with a normal shell +sudo useradd -m -s /bin/bash acme + +# Set the user's password +sudo passwd acme +``` + +```bash +sudo passwd acme +``` + +```bash +sudo mkdir /etc/haproxy/certs +chown acme:root /etc/haproxy/certs +``` + ```bash #--------------------------------------------------------------------- # Example configuration for a possible web application. See the @@ -59,8 +76,6 @@ global # stats socket /var/lib/haproxy/stats level admin mode 660 #stats socket /var/run/haproxy/admin.sock level admin mode 660 user haproxy group haproxy - setenv ACCOUNT_THUMBPRINT \'\' - # utilize system-wide crypto-policies ssl-default-bind-ciphers PROFILE=SYSTEM ssl-default-server-ciphers PROFILE=SYSTEM diff --git a/src/LetsEncryptServer/Configuration.cs b/src/LetsEncryptServer/Configuration.cs index 86ac89a..445b624 100644 --- a/src/LetsEncryptServer/Configuration.cs +++ b/src/LetsEncryptServer/Configuration.cs @@ -1,15 +1,14 @@ namespace MaksIT.LetsEncryptServer { - public class SSHClientConfing { - public required string User { get; set; } - public required string Key { get; set; } - } - public class Server { public required string Ip { get; set; } - public required int Port { get; set; } - public string Path { get; set; } - public required SSHClientConfing SSH { get; set; } + public required int SocketPort { get; set; } + public required int SSHPort { get; set; } + public required string Path { get; set; } + + public required string Username { get; set; } + public string? Password { get; set; } + public string[]? PrivateKeys { get; set; } } public class Configuration { diff --git a/src/LetsEncryptServer/Services/CertsFlowService.cs b/src/LetsEncryptServer/Services/CertsFlowService.cs index 81aa80b..bb2646e 100644 --- a/src/LetsEncryptServer/Services/CertsFlowService.cs +++ b/src/LetsEncryptServer/Services/CertsFlowService.cs @@ -7,8 +7,7 @@ using DomainResults.Common; using MaksIT.LetsEncrypt.Entities; using MaksIT.LetsEncrypt.Services; using MaksIT.LetsEncryptServer.Models.Requests; -using System.Security.Cryptography.X509Certificates; -using System.Security.Cryptography; +using MaksIT.SSHProvider; namespace MaksIT.LetsEncryptServer.Services; @@ -116,41 +115,30 @@ public class CertsFlowService : ICertsFlowService { } public (Dictionary?, IDomainResult) ApplyCertificates(Guid sessionId, GetCertificatesRequest requestData) { - var haproxyHelper = new HaproxyCertificateUpdater(); - - var result = new Dictionary(); + var results = new Dictionary(); foreach (var subject in requestData.Hostnames) { - var (cert, getCertResult) = _letsEncryptService.TryGetCachedCertificate(sessionId, subject); if (!getCertResult.IsSuccess || cert == null) return (null, getCertResult); - //haproxyHelper.ApplyCertificates(subject, cert.Certificate, cert.PrivateKeyPem); var content = $"{cert.Certificate}\n{cert.PrivateKeyPem}"; - result.Add(subject, content); + results.Add(subject, content); } - return IDomainResult.Success(result); + var uploadResult = UploadToServer(results); + if (!uploadResult.IsSuccess) + return (null, uploadResult); + + var notifyResult = NotifyHaproxy(results.Select(x => x.Key)); + if (!notifyResult.IsSuccess) + return (null, notifyResult); + + return IDomainResult.Success(results); } - public (string?, IDomainResult) AcmeChallenge(string fileName) { - - //var currentDate = DateTime.Now; - - //foreach (var file in Directory.GetFiles(_acmePath)) { - // var creationTime = System.IO.File.GetCreationTime(file); - - // // Calculate the time difference - // var timeDifference = currentDate - creationTime; - - // // If the file is older than 1 day, delete it - // if (timeDifference.TotalDays > 1) { - // File.Delete(file); - // _logger.LogInformation($"Deleted file: {file}"); - // } - //} + DeleteExporedChallenges(); var fileContent = File.ReadAllText(Path.Combine(_acmePath, fileName)); if (fileContent == null) @@ -159,49 +147,192 @@ public class CertsFlowService : ICertsFlowService { return IDomainResult.Success(fileContent); } - -} - - - - - -public class HaproxyCertificateUpdater { - private readonly string haproxySocketAddress = "192.168.1.4"; - private readonly int haproxySocketPort = 9999; - - public void ApplyCertificates(string subject, string certPem, string keyPem) { - if (string.IsNullOrEmpty(certPem) || string.IsNullOrEmpty(keyPem)) { - Console.WriteLine($"Certificate or key for {subject} is invalid"); - return; - } - - string certFileName = $"/etc/haproxy/certs/{subject}.pem"; - string fullCert = $"{certPem}\n{keyPem}"; + private IDomainResult UploadToServer(Dictionary results) { + var server = _appSettings.Server; try { - SendCommand($"new ssl cert {certFileName}"); - SendCommand($"set ssl cert {certFileName} <<\n{fullCert}\n"); - SendCommand($"commit ssl cert {certFileName}"); + 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")) { - Console.WriteLine($"Certificate for {subject} updated successfully"); + 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) { - Console.WriteLine($"Exception while updating certificate for {subject}: {ex.Message}"); + var message = "Unable to upload files to remote server"; + _logger.LogError(ex, message); + + return IDomainResult.CriticalDependencyError(message); } + + return IDomainResult.Success(); } - private void SendCommand(string command) { - using (var client = new TcpClient(haproxySocketAddress, haproxySocketPort)) - using (var stream = client.GetStream()) - using (var writer = new StreamWriter(stream)) - using (var reader = new StreamReader(stream)) { - writer.WriteLine(command); - writer.Flush(); - string response = reader.ReadToEnd(); - if (!response.Contains("Success")) { - throw new Exception($"Command failed: {response}"); + /** + abort ssl cert : abort a transaction for a certificate file + add acl [@] : add an acl entry + add map [@] : add a map entry (payload supported instead of key/val) + add ssl crt-list [opts]* : add to crt-list file a line or a payload + clear acl [@] : clear the contents of this acl + clear counters [all] : clear max statistics counters (or all counters) + clear map [@] : clear the contents of this map + clear table []* : remove an entry from a table (filter: data/key) + commit acl @ : commit the ACL at this version + commit map @ : commit the map at this version + commit ssl cert : commit a certificate file + del acl [|#] : delete acl entries matching + del map [|#] : delete map entries matching + del ssl cert : delete an unused certificate file + del ssl crt-list : delete a line from crt-list file + disable agent : disable agent checks + disable dynamic-cookie backend : disable dynamic cookies on a specific backend + disable frontend : temporarily disable specific frontend + disable health : disable health checks + disable server (DEPRECATED) : disable a server for maintenance (use 'set server' instead) + enable agent : enable agent checks + enable dynamic-cookie backend : enable dynamic cookies on a specific backend + enable frontend : re-enable specific frontend + enable health : enable health checks + enable server (DEPRECATED) : enable a disabled server (use 'set server' instead) + get acl : report the patterns matching a sample for an ACL + get map : report the keys and values matching a sample for a map + get var : retrieve contents of a process-wide variable + get weight / : report a server's current weight + new ssl cert : create a new certificate file to be used in a crt-list or a directory + operator : lower the level of the current CLI session to operator + prepare acl : prepare a new version for atomic ACL replacement + prepare map : prepare a new version for atomic map replacement + set dynamic-cookie-key backend : change a backend secret key for dynamic cookies + set map [|#] : modify a map entry + set maxconn frontend : change a frontend's maxconn setting + set maxconn global : change the per-process maxconn setting + set maxconn server / : change a server's maxconn setting + set profiling {auto|on|off} : enable/disable resource profiling (tasks,memory) + set rate-limit : change a rate limiting value + set server / [opts] : change a server's state, weight, address or ssl + set severity-output [none|number|string]: set presence of severity level in feedback information + set ssl cert : replace a certificate file + set ssl ocsp-response : update a certificate's OCSP Response from a base64-encode DER + set ssl tls-key [id|file] : set the next TLS key for the or listener to + set table
key [data.* ]* : update or create a table entry's data + set timeout [cli] : change a timeout setting + set weight / (DEPRECATED) : change a server's weight (use 'set server' instead) + show acl [@] ] : report available acls or dump an acl's contents + show activity : show per-thread activity stats (for support/developers) + show backend : list backends in the current running config + show cache : show cache status + show cli level : display the level of the current CLI session + show cli sockets : dump list of cli sockets + show env [var] : dump environment variables known to the process + show errors [] [request|response] : report last request and/or response errors for each proxy + show events [] [-w] [-n] : show event sink state + show fd [num] : dump list of file descriptors in use or a specific one + show info [desc|json|typed|float]* : report information about the running process + show libs : show loaded object files and libraries + show map [@ver] [map] : report available maps or dump a map's contents + show peers [dict|-] [section] : dump some information about all the peers or this peers section + show pools : report information about the memory pools usage + show profiling [|<#lines>|byaddr]*: show profiling state (all,status,tasks,memory) + show resolvers [id] : dumps counters from all resolvers section and associated name servers + show schema json : report schema used for stats + show servers conn [] : dump server connections status (all or for a single backend) + show servers state [] : dump volatile server information (all or for a single backend) + show sess [id] : report the list of current sessions or dump this exact session + show ssl cert [] : display the SSL certificates used in memory, or the details of a file + show ssl crt-list [-n] [] : show the list of crt-lists or the content of a crt-list file + show startup-logs : report logs emitted during HAProxy startup + show stat [desc|json|no-maint|typed|up]*: report counters for each proxy and server + show table
[]* : report table usage stats or dump this table's contents (filter: data/key) + show tasks : show running tasks + show threads : show some threads debugging information + show tls-keys [id|*] : show tls keys references or dump tls ticket keys when id specified + show trace [] : show live tracing state + show version : show version of the current process + shutdown frontend : stop a specific frontend + shutdown session [id] : kill a specific session + shutdown sessions server / : kill sessions on a server + trace [|0] [cmd [args...]] : manage live tracing (empty to list, 0 to stop all) + user : lower the level of the current CLI session to user + help [] : list matching or all commands + prompt : toggle interactive mode with prompt + quit : disconnect + */ + private IDomainResult NotifyHaproxy(IEnumerable certFiles) { + 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 certFile in certFiles) { + + // Prepare the certificate + string prepareCommand = $"new ssl cert {server.Path}/{certFile}\n"; + writer.WriteLine(prepareCommand); + 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}"); + } + + // Commit the certificate + string commitCommand = $"commit ssl cert {server.Path}/{certFile}\n"; + writer.WriteLine(commitCommand); + 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; + + foreach (var file in Directory.GetFiles(_acmePath)) { + try { + var creationTime = File.GetCreationTime(file); + + // Calculate the time difference + var timeDifference = currentDate - creationTime; + + // If the file is older than 1 day, delete it + if (timeDifference.TotalDays > 1) { + + + File.Delete(file); + _logger.LogInformation($"Deleted file: {file}"); + } + } + catch (Exception ex) { + _logger.LogWarning(ex, "File cannot be deleted"); } } } diff --git a/src/LetsEncryptServer/appsettings.json b/src/LetsEncryptServer/appsettings.json index 5697a0c..a9fcddf 100644 --- a/src/LetsEncryptServer/appsettings.json +++ b/src/LetsEncryptServer/appsettings.json @@ -15,12 +15,12 @@ "Server": { "Ip": "192.168.1.4", - "Port": 9999, + "SocketPort": 9999, + "SSHPort": 22, "Path": "/etc/haproxy/certs", - "SSH": { - "User": "", - "Key": "" - } + "Username": "acme", + "PrivateKeys": [], + "Password": "acme" } } }