Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): Add certificates to Operator.Web #756

Merged
merged 11 commits into from
May 15, 2024
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
Loading