mirror of
https://github.com/MAKS-IT-COM/maksit-certs-ui.git
synced 2025-12-31 04:00:03 +01:00
(feature): remote server cert upload
This commit is contained in:
parent
d75265c621
commit
a661489b4f
@ -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"
|
||||
|
||||
19
README.md
19
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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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<string, string>?, IDomainResult) ApplyCertificates(Guid sessionId, GetCertificatesRequest requestData) {
|
||||
var haproxyHelper = new HaproxyCertificateUpdater();
|
||||
|
||||
var result = new Dictionary<string, string>();
|
||||
var results = new Dictionary<string, string>();
|
||||
|
||||
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<string, string> 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();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
abort ssl cert <certfile> : abort a transaction for a certificate file
|
||||
add acl [@<ver>] <acl> <pattern> : add an acl entry
|
||||
add map [@<ver>] <map> <key> <val> : add a map entry (payload supported instead of key/val)
|
||||
add ssl crt-list <list> <cert> [opts]* : add to crt-list file <list> a line <cert> or a payload
|
||||
clear acl [@<ver>] <acl> : clear the contents of this acl
|
||||
clear counters [all] : clear max statistics counters (or all counters)
|
||||
clear map [@<ver>] <map> : clear the contents of this map
|
||||
clear table <table> [<filter>]* : remove an entry from a table (filter: data/key)
|
||||
commit acl @<ver> <acl> : commit the ACL at this version
|
||||
commit map @<ver> <map> : commit the map at this version
|
||||
commit ssl cert <certfile> : commit a certificate file
|
||||
del acl <acl> [<key>|#<ref>] : delete acl entries matching <key>
|
||||
del map <map> [<key>|#<ref>] : delete map entries matching <key>
|
||||
del ssl cert <certfile> : delete an unused certificate file
|
||||
del ssl crt-list <list> <cert[:line]> : delete a line <cert> from crt-list file <list>
|
||||
disable agent : disable agent checks
|
||||
disable dynamic-cookie backend <bk> : disable dynamic cookies on a specific backend
|
||||
disable frontend <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 <bk> : enable dynamic cookies on a specific backend
|
||||
enable frontend <frontend> : re-enable specific frontend
|
||||
enable health : enable health checks
|
||||
enable server (DEPRECATED) : enable a disabled server (use 'set server' instead)
|
||||
get acl <acl> <value> : report the patterns matching a sample for an ACL
|
||||
get map <acl> <value> : report the keys and values matching a sample for a map
|
||||
get var <name> : retrieve contents of a process-wide variable
|
||||
get weight <bk>/<srv> : report a server's current weight
|
||||
new ssl cert <certfile> : 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 <acl> : prepare a new version for atomic ACL replacement
|
||||
prepare map <acl> : prepare a new version for atomic map replacement
|
||||
set dynamic-cookie-key backend <bk> <k> : change a backend secret key for dynamic cookies
|
||||
set map <map> [<key>|#<ref>] <value> : modify a map entry
|
||||
set maxconn frontend <frontend> <value> : change a frontend's maxconn setting
|
||||
set maxconn global <value> : change the per-process maxconn setting
|
||||
set maxconn server <bk>/<srv> : change a server's maxconn setting
|
||||
set profiling <what> {auto|on|off} : enable/disable resource profiling (tasks,memory)
|
||||
set rate-limit <setting> <value> : change a rate limiting value
|
||||
set server <bk>/<srv> [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 <certfile> <payload> : replace a certificate file
|
||||
set ssl ocsp-response <resp|payload> : update a certificate's OCSP Response from a base64-encode DER
|
||||
set ssl tls-key [id|file] <key> : set the next TLS key for the <id> or <file> listener to <key>
|
||||
set table <table> key <k> [data.* <v>]* : update or create a table entry's data
|
||||
set timeout [cli] <delay> : change a timeout setting
|
||||
set weight <bk>/<srv> (DEPRECATED) : change a server's weight (use 'set server' instead)
|
||||
show acl [@<ver>] <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 [<px>] [request|response] : report last request and/or response errors for each proxy
|
||||
show events [<sink>] [-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 [<what>|<#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 [<backend>] : dump server connections status (all or for a single backend)
|
||||
show servers state [<backend>] : 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 [<certfile>] : display the SSL certificates used in memory, or the details of a file
|
||||
show ssl crt-list [-n] [<list>] : show the list of crt-lists or the content of a crt-list file <list>
|
||||
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 <table> [<filter>]* : 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 [<module>] : show live tracing state
|
||||
show version : show version of the current process
|
||||
shutdown frontend <frontend> : stop a specific frontend
|
||||
shutdown session [id] : kill a specific session
|
||||
shutdown sessions server <bk>/<srv> : kill sessions on a server
|
||||
trace [<module>|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 [<command>] : list matching or all commands
|
||||
prompt : toggle interactive mode with prompt
|
||||
quit : disconnect
|
||||
*/
|
||||
private IDomainResult NotifyHaproxy(IEnumerable<string> 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}");
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
_logger.LogInformation("Certificates committed successfully.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
var message = "An error occurred while committing certificates";
|
||||
_logger.LogError(ex, message);
|
||||
|
||||
string response = reader.ReadToEnd();
|
||||
if (!response.Contains("Success")) {
|
||||
throw new Exception($"Command failed: {response}");
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user