Skip to content

Commit af36e6e

Browse files
authored
feat(core): Add certificates to Operator.Web (#756)
This adds a new CertificateGenerator to Operator.Web, and it reworks a handful of the classes in Operator.Web to support using certificates in code. I more or less re-implemented the BouncyCastle solution from the CLI in Operator.Web using the built-in .NET classes, and I went to some length to ensure the generated certificates are virtually identical to those produced by BouncyCastle.
1 parent ad80d80 commit af36e6e

22 files changed

+734
-333
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
using System.Security.Cryptography;
2+
using System.Security.Cryptography.X509Certificates;
3+
4+
namespace KubeOps.Abstractions.Certificates
5+
{
6+
public record CertificatePair(X509Certificate2 Certificate, AsymmetricAlgorithm Key);
7+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System.Security.Cryptography;
2+
using System.Security.Cryptography.X509Certificates;
3+
4+
namespace KubeOps.Abstractions.Certificates
5+
{
6+
/// <summary>
7+
/// Defines properties for certificate/key pair so a custom certificate/key provider may be implemented.
8+
/// The provider is used by the CertificateWebhookService to provide a caBundle to the webhooks.
9+
/// </summary>
10+
public interface ICertificateProvider : IDisposable
11+
{
12+
/// <summary>
13+
/// The server certificate and key.
14+
/// </summary>
15+
CertificatePair Server { get; }
16+
17+
/// <summary>
18+
/// The root certificate and key.
19+
/// </summary>
20+
CertificatePair Root { get; }
21+
}
22+
}

src/KubeOps.Cli/Certificates/CertificateGenerator.cs

Lines changed: 0 additions & 129 deletions
This file was deleted.

src/KubeOps.Cli/Certificates/Extensions.cs

Lines changed: 0 additions & 22 deletions
This file was deleted.
Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,17 @@
1-
using KubeOps.Cli.Certificates;
21
using KubeOps.Cli.Output;
2+
using KubeOps.Operator.Web.Certificates;
33

44
namespace KubeOps.Cli.Generators;
55

66
internal class CertificateGenerator(string serverName, string namespaceName) : IConfigGenerator
77
{
88
public void Generate(ResultOutput output)
99
{
10-
var (caCert, caKey) = Certificates.CertificateGenerator.CreateCaCertificate();
10+
using Operator.Web.CertificateGenerator generator = new(serverName, namespaceName);
1111

12-
output.Add("ca.pem", caCert.ToPem(), OutputFormat.Plain);
13-
output.Add("ca-key.pem", caKey.ToPem(), OutputFormat.Plain);
14-
15-
var (srvCert, srvKey) = Certificates.CertificateGenerator.CreateServerCertificate(
16-
(caCert, caKey),
17-
serverName,
18-
namespaceName);
19-
20-
output.Add("svc.pem", srvCert.ToPem(), OutputFormat.Plain);
21-
output.Add("svc-key.pem", srvKey.ToPem(), OutputFormat.Plain);
12+
output.Add("ca.pem", generator.Root.Certificate.EncodeToPem(), OutputFormat.Plain);
13+
output.Add("ca-key.pem", generator.Root.Key.EncodeToPem(), OutputFormat.Plain);
14+
output.Add("svc.pem", generator.Server.Certificate.EncodeToPem(), OutputFormat.Plain);
15+
output.Add("svc-key.pem", generator.Server.Key.EncodeToPem(), OutputFormat.Plain);
2216
}
2317
}

src/KubeOps.Cli/KubeOps.Cli.csproj

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
@@ -18,7 +18,6 @@
1818
</PropertyGroup>
1919

2020
<ItemGroup>
21-
<PackageReference Include="BouncyCastle.Cryptography" Version="2.3.0" />
2221
<PackageReference Include="Microsoft.Build.Locator" Version="1.7.1" />
2322
<PackageReference Include="Microsoft.CodeAnalysis" Version="4.8.0" />
2423
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.8.0" />
@@ -34,7 +33,7 @@
3433
</ItemGroup>
3534

3635
<ItemGroup>
37-
<ProjectReference Include="..\KubeOps.Abstractions\KubeOps.Abstractions.csproj"/>
36+
<ProjectReference Include="..\KubeOps.Abstractions\KubeOps.Abstractions.csproj" />
3837
<ProjectReference Include="..\KubeOps.Operator.Web\KubeOps.Operator.Web.csproj" />
3938
<ProjectReference Include="..\KubeOps.Transpiler\KubeOps.Transpiler.csproj" />
4039
</ItemGroup>

src/KubeOps.Operator.Web/Builder/OperatorBuilderExtensions.cs

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
using System.Runtime.Versioning;
33

44
using KubeOps.Abstractions.Builder;
5+
using KubeOps.Abstractions.Certificates;
6+
using KubeOps.Operator.Web.Certificates;
57
using KubeOps.Operator.Web.LocalTunnel;
8+
using KubeOps.Operator.Web.Webhooks;
69

710
using Microsoft.Extensions.DependencyInjection;
811

@@ -39,16 +42,67 @@ public static class OperatorBuilderExtensions
3942
/// </code>
4043
/// </example>
4144
[RequiresPreviewFeatures(
42-
"Localtunnel is sometimes unstable, use with caution. " +
43-
"This API is in preview and may be removed in future versions if no stable alternative is found.")]
45+
"LocalTunnel is sometimes unstable, use with caution.")]
46+
#pragma warning disable S1133 // Deprecated code should be removed
47+
[Obsolete(
48+
"LocalTunnel features are deprecated and will be removed in a future version. " +
49+
$"Instead, use the {nameof(UseCertificateProvider)} method for development webhooks.")]
50+
#pragma warning restore S1133 // Deprecated code should be removed
4451
public static IOperatorBuilder AddDevelopmentTunnel(
4552
this IOperatorBuilder builder,
4653
ushort port,
4754
string hostname = "localhost")
4855
{
49-
builder.Services.AddHostedService<DevelopmentTunnelService>();
50-
builder.Services.AddSingleton(new TunnelConfig(hostname, port));
56+
builder.Services.AddHostedService<TunnelWebhookService>();
5157
builder.Services.AddSingleton(new WebhookLoader(Assembly.GetEntryAssembly()!));
58+
builder.Services.AddSingleton(new WebhookConfig(hostname, port));
59+
builder.Services.AddSingleton<DevelopmentTunnel>();
60+
61+
return builder;
62+
}
63+
64+
/// <summary>
65+
/// Adds a hosted service to the system that uses the server certificate from an <see cref="ICertificateProvider"/>
66+
/// implementation to configure development webhooks. The webhooks will be configured to use the hostname and port.
67+
/// </summary>
68+
/// <param name="builder">The operator builder.</param>
69+
/// <param name="port">The port that the webhooks will use to connect to the operator.</param>
70+
/// <param name="hostname">The hostname, IP, or FQDN of the machine running the operator.</param>
71+
/// <param name="certificateProvider">The <see cref="ICertificateProvider"/> the <see cref="CertificateWebhookService"/>
72+
/// will use to generate the PEM-encoded server certificate for the webhooks.</param>
73+
/// <returns>The builder for chaining.</returns>
74+
/// <example>
75+
/// Use the development webhooks.
76+
/// <code>
77+
/// var builder = WebApplication.CreateBuilder(args);
78+
/// string ip = "192.168.1.100";
79+
/// ushort port = 443;
80+
///
81+
/// using CertificateGenerator generator = new CertificateGenerator(ip);
82+
/// using X509Certificate2 cert = generator.Server.CopyServerCertWithPrivateKey();
83+
/// // Configure Kestrel to listen on IPv4, use port 443, and use the server certificate
84+
/// builder.WebHost.ConfigureKestrel(serverOptions =>
85+
/// {
86+
/// serverOptions.Listen(System.Net.IPAddress.Any, port, async listenOptions =>
87+
/// {
88+
/// listenOptions.UseHttps(cert);
89+
/// });
90+
/// });
91+
/// builder.Services
92+
/// .AddKubernetesOperator()
93+
/// // Create the development webhook service using the cert provider
94+
/// .UseCertificateProvider(port, ip, generator)
95+
/// // More code
96+
///
97+
/// </code>
98+
/// </example>
99+
public static IOperatorBuilder UseCertificateProvider(this IOperatorBuilder builder, ushort port, string hostname, ICertificateProvider certificateProvider)
100+
{
101+
builder.Services.AddHostedService<CertificateWebhookService>();
102+
builder.Services.AddSingleton(new WebhookLoader(Assembly.GetEntryAssembly()!));
103+
builder.Services.AddSingleton(new WebhookConfig(hostname, port));
104+
builder.Services.AddSingleton(certificateProvider);
105+
52106
return builder;
53107
}
54108
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using System.Security.Cryptography;
2+
using System.Security.Cryptography.X509Certificates;
3+
using System.Text;
4+
5+
using KubeOps.Abstractions.Certificates;
6+
7+
namespace KubeOps.Operator.Web.Certificates
8+
{
9+
public static class CertificateExtensions
10+
{
11+
/// <summary>
12+
/// Encodes the certificate in PEM format for use in Kubernetes.
13+
/// </summary>
14+
/// <param name="certificate">The certificate to encode.</param>
15+
/// <returns>The byte representation of the PEM-encoded certificate.</returns>
16+
public static byte[] EncodeToPemBytes(this X509Certificate2 certificate) => Encoding.UTF8.GetBytes(certificate.EncodeToPem());
17+
18+
/// <summary>
19+
/// Encodes the certificate in PEM format.
20+
/// </summary>
21+
/// <param name="certificate">The certificate to encode.</param>
22+
/// <returns>The string representation of the PEM-encoded certificate.</returns>
23+
public static string EncodeToPem(this X509Certificate2 certificate) => new(PemEncoding.Write("CERTIFICATE", certificate.RawData));
24+
25+
/// <summary>
26+
/// Encodes the key in PEM format.
27+
/// </summary>
28+
/// <param name="key">The key to encode.</param>
29+
/// <returns>The string representation of the PEM-encoded key.</returns>
30+
public static string EncodeToPem(this AsymmetricAlgorithm key) => new(PemEncoding.Write("PRIVATE KEY", key.ExportPkcs8PrivateKey()));
31+
32+
/// <summary>
33+
/// Generates a new server certificate with its private key attached, and sets <see cref="X509KeyStorageFlags.PersistKeySet"/>.
34+
/// For example, this certificate can be used in development environments to configure <see cref="Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions"/>.
35+
/// </summary>
36+
/// <param name="serverPair">The cert/key tuple to attach.</param>
37+
/// <returns>An <see cref="X509Certificate2"/> with the private key attached.</returns>
38+
/// <exception cref="NotImplementedException">The <see cref="AsymmetricAlgorithm"/> not have a CopyWithPrivateKey method, or the
39+
/// method has not been implemented in this extension.</exception>
40+
public static X509Certificate2 CopyServerCertWithPrivateKey(this CertificatePair serverPair)
41+
{
42+
const string? password = null;
43+
using X509Certificate2 temp = serverPair.Key switch
44+
{
45+
ECDsa ecdsa => serverPair.Certificate.CopyWithPrivateKey(ecdsa),
46+
RSA rsa => serverPair.Certificate.CopyWithPrivateKey(rsa),
47+
ECDiffieHellman ecdh => serverPair.Certificate.CopyWithPrivateKey(ecdh),
48+
DSA dsa => serverPair.Certificate.CopyWithPrivateKey(dsa),
49+
_ => throw new NotImplementedException($"{serverPair.Key} is not implemented for {nameof(CopyServerCertWithPrivateKey)}"),
50+
};
51+
52+
return new X509Certificate2(
53+
temp.Export(X509ContentType.Pfx, password),
54+
password,
55+
X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet);
56+
}
57+
}
58+
}

0 commit comments

Comments
 (0)