Skip to content

Commit

Permalink
feat(core): Add certificates to Operator.Web (#756)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ian-buse committed May 15, 2024
1 parent ad80d80 commit af36e6e
Show file tree
Hide file tree
Showing 22 changed files with 734 additions and 333 deletions.
7 changes: 7 additions & 0 deletions src/KubeOps.Abstractions/Certificates/CertificatePair.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

namespace KubeOps.Abstractions.Certificates
{
public record CertificatePair(X509Certificate2 Certificate, AsymmetricAlgorithm Key);
}
22 changes: 22 additions & 0 deletions src/KubeOps.Abstractions/Certificates/ICertificateProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

namespace KubeOps.Abstractions.Certificates
{
/// <summary>
/// Defines properties for certificate/key pair so a custom certificate/key provider may be implemented.
/// The provider is used by the CertificateWebhookService to provide a caBundle to the webhooks.
/// </summary>
public interface ICertificateProvider : IDisposable
{
/// <summary>
/// The server certificate and key.
/// </summary>
CertificatePair Server { get; }

/// <summary>
/// The root certificate and key.
/// </summary>
CertificatePair Root { get; }
}
}
129 changes: 0 additions & 129 deletions src/KubeOps.Cli/Certificates/CertificateGenerator.cs

This file was deleted.

22 changes: 0 additions & 22 deletions src/KubeOps.Cli/Certificates/Extensions.cs

This file was deleted.

18 changes: 6 additions & 12 deletions src/KubeOps.Cli/Generators/CertificateGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@
using KubeOps.Cli.Certificates;
using KubeOps.Cli.Output;
using KubeOps.Operator.Web.Certificates;

namespace KubeOps.Cli.Generators;

internal class CertificateGenerator(string serverName, string namespaceName) : IConfigGenerator
{
public void Generate(ResultOutput output)
{
var (caCert, caKey) = Certificates.CertificateGenerator.CreateCaCertificate();
using Operator.Web.CertificateGenerator generator = new(serverName, namespaceName);

output.Add("ca.pem", caCert.ToPem(), OutputFormat.Plain);
output.Add("ca-key.pem", caKey.ToPem(), OutputFormat.Plain);

var (srvCert, srvKey) = Certificates.CertificateGenerator.CreateServerCertificate(
(caCert, caKey),
serverName,
namespaceName);

output.Add("svc.pem", srvCert.ToPem(), OutputFormat.Plain);
output.Add("svc-key.pem", srvKey.ToPem(), OutputFormat.Plain);
output.Add("ca.pem", generator.Root.Certificate.EncodeToPem(), OutputFormat.Plain);
output.Add("ca-key.pem", generator.Root.Key.EncodeToPem(), OutputFormat.Plain);
output.Add("svc.pem", generator.Server.Certificate.EncodeToPem(), OutputFormat.Plain);
output.Add("svc-key.pem", generator.Server.Key.EncodeToPem(), OutputFormat.Plain);
}
}
5 changes: 2 additions & 3 deletions src/KubeOps.Cli/KubeOps.Cli.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
Expand All @@ -18,7 +18,6 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BouncyCastle.Cryptography" Version="2.3.0" />
<PackageReference Include="Microsoft.Build.Locator" Version="1.7.1" />
<PackageReference Include="Microsoft.CodeAnalysis" Version="4.8.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.8.0" />
Expand All @@ -34,7 +33,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\KubeOps.Abstractions\KubeOps.Abstractions.csproj"/>
<ProjectReference Include="..\KubeOps.Abstractions\KubeOps.Abstractions.csproj" />
<ProjectReference Include="..\KubeOps.Operator.Web\KubeOps.Operator.Web.csproj" />
<ProjectReference Include="..\KubeOps.Transpiler\KubeOps.Transpiler.csproj" />
</ItemGroup>
Expand Down
62 changes: 58 additions & 4 deletions src/KubeOps.Operator.Web/Builder/OperatorBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
using System.Runtime.Versioning;

using KubeOps.Abstractions.Builder;
using KubeOps.Abstractions.Certificates;
using KubeOps.Operator.Web.Certificates;
using KubeOps.Operator.Web.LocalTunnel;
using KubeOps.Operator.Web.Webhooks;

using Microsoft.Extensions.DependencyInjection;

Expand Down Expand Up @@ -39,16 +42,67 @@ public static class OperatorBuilderExtensions
/// </code>
/// </example>
[RequiresPreviewFeatures(
"Localtunnel is sometimes unstable, use with caution. " +
"This API is in preview and may be removed in future versions if no stable alternative is found.")]
"LocalTunnel is sometimes unstable, use with caution.")]
#pragma warning disable S1133 // Deprecated code should be removed
[Obsolete(
"LocalTunnel features are deprecated and will be removed in a future version. " +
$"Instead, use the {nameof(UseCertificateProvider)} method for development webhooks.")]
#pragma warning restore S1133 // Deprecated code should be removed
public static IOperatorBuilder AddDevelopmentTunnel(
this IOperatorBuilder builder,
ushort port,
string hostname = "localhost")
{
builder.Services.AddHostedService<DevelopmentTunnelService>();
builder.Services.AddSingleton(new TunnelConfig(hostname, port));
builder.Services.AddHostedService<TunnelWebhookService>();
builder.Services.AddSingleton(new WebhookLoader(Assembly.GetEntryAssembly()!));
builder.Services.AddSingleton(new WebhookConfig(hostname, port));
builder.Services.AddSingleton<DevelopmentTunnel>();

return builder;
}

/// <summary>
/// Adds a hosted service to the system that uses the server certificate from an <see cref="ICertificateProvider"/>
/// implementation to configure development webhooks. The webhooks will be configured to use the hostname and port.
/// </summary>
/// <param name="builder">The operator builder.</param>
/// <param name="port">The port that the webhooks will use to connect to the operator.</param>
/// <param name="hostname">The hostname, IP, or FQDN of the machine running the operator.</param>
/// <param name="certificateProvider">The <see cref="ICertificateProvider"/> the <see cref="CertificateWebhookService"/>
/// will use to generate the PEM-encoded server certificate for the webhooks.</param>
/// <returns>The builder for chaining.</returns>
/// <example>
/// Use the development webhooks.
/// <code>
/// var builder = WebApplication.CreateBuilder(args);
/// string ip = "192.168.1.100";
/// ushort port = 443;
///
/// using CertificateGenerator generator = new CertificateGenerator(ip);
/// using X509Certificate2 cert = generator.Server.CopyServerCertWithPrivateKey();
/// // Configure Kestrel to listen on IPv4, use port 443, and use the server certificate
/// builder.WebHost.ConfigureKestrel(serverOptions =>
/// {
/// serverOptions.Listen(System.Net.IPAddress.Any, port, async listenOptions =>
/// {
/// listenOptions.UseHttps(cert);
/// });
/// });
/// builder.Services
/// .AddKubernetesOperator()
/// // Create the development webhook service using the cert provider
/// .UseCertificateProvider(port, ip, generator)
/// // More code
///
/// </code>
/// </example>
public static IOperatorBuilder UseCertificateProvider(this IOperatorBuilder builder, ushort port, string hostname, ICertificateProvider certificateProvider)
{
builder.Services.AddHostedService<CertificateWebhookService>();
builder.Services.AddSingleton(new WebhookLoader(Assembly.GetEntryAssembly()!));
builder.Services.AddSingleton(new WebhookConfig(hostname, port));
builder.Services.AddSingleton(certificateProvider);

return builder;
}
}
58 changes: 58 additions & 0 deletions src/KubeOps.Operator.Web/Certificates/CertificateExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;

using KubeOps.Abstractions.Certificates;

namespace KubeOps.Operator.Web.Certificates
{
public static class CertificateExtensions
{
/// <summary>
/// Encodes the certificate in PEM format for use in Kubernetes.
/// </summary>
/// <param name="certificate">The certificate to encode.</param>
/// <returns>The byte representation of the PEM-encoded certificate.</returns>
public static byte[] EncodeToPemBytes(this X509Certificate2 certificate) => Encoding.UTF8.GetBytes(certificate.EncodeToPem());

/// <summary>
/// Encodes the certificate in PEM format.
/// </summary>
/// <param name="certificate">The certificate to encode.</param>
/// <returns>The string representation of the PEM-encoded certificate.</returns>
public static string EncodeToPem(this X509Certificate2 certificate) => new(PemEncoding.Write("CERTIFICATE", certificate.RawData));

/// <summary>
/// Encodes the key in PEM format.
/// </summary>
/// <param name="key">The key to encode.</param>
/// <returns>The string representation of the PEM-encoded key.</returns>
public static string EncodeToPem(this AsymmetricAlgorithm key) => new(PemEncoding.Write("PRIVATE KEY", key.ExportPkcs8PrivateKey()));

/// <summary>
/// Generates a new server certificate with its private key attached, and sets <see cref="X509KeyStorageFlags.PersistKeySet"/>.
/// For example, this certificate can be used in development environments to configure <see cref="Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions"/>.
/// </summary>
/// <param name="serverPair">The cert/key tuple to attach.</param>
/// <returns>An <see cref="X509Certificate2"/> with the private key attached.</returns>
/// <exception cref="NotImplementedException">The <see cref="AsymmetricAlgorithm"/> not have a CopyWithPrivateKey method, or the
/// method has not been implemented in this extension.</exception>
public static X509Certificate2 CopyServerCertWithPrivateKey(this CertificatePair serverPair)
{
const string? password = null;
using X509Certificate2 temp = serverPair.Key switch
{
ECDsa ecdsa => serverPair.Certificate.CopyWithPrivateKey(ecdsa),
RSA rsa => serverPair.Certificate.CopyWithPrivateKey(rsa),
ECDiffieHellman ecdh => serverPair.Certificate.CopyWithPrivateKey(ecdh),
DSA dsa => serverPair.Certificate.CopyWithPrivateKey(dsa),
_ => throw new NotImplementedException($"{serverPair.Key} is not implemented for {nameof(CopyServerCertWithPrivateKey)}"),
};

return new X509Certificate2(
temp.Export(X509ContentType.Pfx, password),
password,
X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet);
}
}
}
Loading

0 comments on commit af36e6e

Please sign in to comment.