Skip to content

Commit

Permalink
added support for binary data in kamus secret (#248)
Browse files Browse the repository at this point in the history
* added support for binary data in kamus secret

* documentaion

* fix the tests

* fix the tests build

* try to fix the test

* fix the tests

* rename to encodedData

* refactor - add v1alpha2 which is compatible with k8s secrets

* update docs

* fix the build

* try to fix the tests

* fix the tests

* store only v2

* try to fix the tests

* try to fix the tests

* clean up

* try to fix the build

* added support for conversation webhook

* revert test changes

* try to fix the build

* remove encodedData

* force https for the controller

* temporary - just make it work

* fix the tests

* ugly patch :praying af

* try to fix the build

* no judgment

* enable CustomResourceWebhookConversion

* try to fix the build

* maybe :shrug

* fix the build

* try again

* pleaseeee

* added logging

* clarify docs

* remove certificate from the dockerfile

* this time create the certificate for the proper name

* Update src/crd-controller/Startup.cs

Co-Authored-By: Shai Katz <shaikatz@users.noreply.github.com>

* Update tests/crd-controller/deployment.yaml

Co-Authored-By: Shai Katz <shaikatz@users.noreply.github.com>

* Update src/crd-controller/Models/V1Alpha2/KamusSecret.cs

Co-Authored-By: Shai Katz <shaikatz@users.noreply.github.com>

* fix CR comments

* try to fix the build

* add comments

* version bump
  • Loading branch information
omerlh authored Sep 9, 2019
1 parent a365deb commit 5c7be1c
Show file tree
Hide file tree
Showing 29 changed files with 708 additions and 128 deletions.
2 changes: 2 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ commands:
- docker_api_cache_key-{{ .Revision }}
- run:
name: install
environment:
kubernetesVersion: parameters.kubernetesVersion
command: |
tests/crd-controller/run-tests.sh << parameters.kubernetesVersion >>
no_output_timeout: 3600
Expand Down
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ ENV PROJECT_NAME_ENV=$PROJECT_NAME
RUN addgroup dotnet && \
adduser -D -G dotnet -h /home/dotnet dotnet && \
apk add --update --no-cahce libc6-compat

USER dotnet
WORKDIR /home/dotnet/app
ENV ASPNETCORE_URLS=http://+:9999
COPY --from=build-env /app/$PROJECT_NAME/obj/Docker/publish .

ENTRYPOINT dotnet $PROJECT_NAME_ENV.dll
Binary file added certificate.pfx
Binary file not shown.
17 changes: 15 additions & 2 deletions site/content/docs/user/crd.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ Using KamusSecret allows to use Kamus with applications that requires native Kub

## Usage
KamusSecret works very similary to regular secret encryption flow with Kamus.
The encrypted data is represented in a format that is identical to regular [Kubernetes Secrets].
Kamus will create an identical secret with the decrypted content.

To encrypt the data, start by deciding to which namespace and which service account you're encrypting it.
The service account does not have to exist or used by the pod consuming the secret.
It just used for expressing who can consume this encrypted secret.
Expand All @@ -27,14 +30,16 @@ kamus-cli encrypt
```
Now that you have the data encrypted, create a KamusSecret object, using the following manifest:
```
apiVersion: "soluto.com/v1alpha1"
apiVersion: "soluto.com/v1alpha2"
kind: KamusSecret
metadata:
name: my-tls-secret //This will be the name of the secret
namespace: default //The secret and KamusSecret live in this namespace
type: TlsSecret //The type of the secret that will be created
data: //Put here all the encrypted data, that will be stored (decrypted) on the secret data
stringData: //Put here all the encrypted data, that will be stored (decrypted) on the secret data
key: J9NYLzTC/O44DvlCEZ+LfQ==:Cc9O5zQzFOyxwTD5ZHseqg==
data: //Put here base64 encoded data (usually, binary data like private keys in der format) encrypted (e.g. encrypt the value after base64 encoding it)
key2: J9NYLzTC/O44DvlCEZ+LfQ==:Cc9O5zQzFOyxwTD5ZHseqg==
serviceAccount: some-sa //The service account used for encrypting the data
```
And finally, create the KamusSecret using:
Expand All @@ -49,8 +54,16 @@ default-token-m6whl kubernetes.io/service-account-token
my-tls-secret TlsSecret 1 5s
```

## Migrating from previous version
To migrate from `v1alpha1` to `v1alpha2` all you need to do is:

* Change the key `data` to `stringData`
* Change the `apiVersion` to `"soluto.com/v1alpha2"`

## Known limitation
This is the alpha release of this feature, so not all functionality is supported.
The current known issues:

* There is no validation - so if you forgot to add mandatory keys to the KamusSecret objects, it will not be created properly.

[kubernetes secrets]: (https://kubernetes.io/docs/concepts/configuration/secret/)
112 changes: 112 additions & 0 deletions src/crd-controller/Controllers/ConversionWebhookController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
using System;
using System.Linq;
using CustomResourceDescriptorController.Models;
using k8s.Models;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
using Serilog;

namespace CustomResourceDescriptorController.Controllers
{
public class ConversionWebhookController : Controller
{
private readonly ILogger mLogger = Log.ForContext<ConversionWebhookController>();

[HttpPost]
[Route("/api/v1/conversion-webhook")]
public ActionResult<ConversionReview> Convert([FromBody]ConversionReview conversionReview)
{
ConversionReviewResponse response;

mLogger.Information("Received conversion request");
try
{
response = new ConversionReviewResponse
{
UID = conversionReview.Request.UID,
ConvertedObjects = conversionReview.Request.Objects.Select(o => Convert(o, conversionReview.Request.DesiredAPIVersion)).ToArray(),
Result = new V1Status
{
Status = "Success"
}
};
}
catch (Exception e)
{
mLogger.Error(e, "Coversation failed");
response = new ConversionReviewResponse
{
UID = conversionReview.Request.UID,
Result = new V1Status
{
Status = "Failure",
Message = "Conversation failed, check logs for more details"
}
};
}

return new ConversionReview
{
Kind = conversionReview.Kind,
ApiVersion = conversionReview.ApiVersion,
Response = response
};
}

private object Convert(JObject source, string desiredApiVersion)
{
var apiVersion = source.Value<string>("apiVersion");

mLogger.Information("Starting to convert from {apiVersion} to {desirediVersion}", apiVersion, desiredApiVersion);

switch (desiredApiVersion)
{
case "soluto.com/v1alpha1":
switch (apiVersion)
{
case "soluto.com/v1alpha2":
var sourceKamusSecret = source.ToObject<Models.V1Alpha2.KamusSecret>();
return new Models.V1Alpha1.KamusSecret
{
Data = sourceKamusSecret.StringData,
ServiceAccount = sourceKamusSecret.ServiceAccount,
Metadata = sourceKamusSecret.Metadata,
Kind = "KamusSecret",
Type = sourceKamusSecret.Type,
ApiVersion = desiredApiVersion
};

default:
throw new InvalidOperationException($"Unsupported conversation from {apiVersion} to {desiredApiVersion}");
}


case "soluto.com/v1alpha2":

switch (apiVersion)
{
case "soluto.com/v1alpha1":
var sourceKamusSecret = source.ToObject<Models.V1Alpha1.KamusSecret>();
return new Models.V1Alpha2.KamusSecret
{
StringData = sourceKamusSecret.Data,
ServiceAccount = sourceKamusSecret.ServiceAccount,
Metadata = sourceKamusSecret.Metadata,
Kind = "KamusSecret",
Type = sourceKamusSecret.Type,
ApiVersion = desiredApiVersion
};

default:
throw new InvalidOperationException($"Unsupported conversation from {apiVersion} to {desiredApiVersion}");
}

default:
throw new InvalidOperationException($"Unsupported conversation from {apiVersion} to {desiredApiVersion}");
}

}
}


}
180 changes: 180 additions & 0 deletions src/crd-controller/HostedServices/V1Alpha1Controller.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
using System;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using CustomResourceDescriptorController.Models.V1Alpha1;
using k8s;
using k8s.Models;
using Kamus.KeyManagement;
using CustomResourceDescriptorController.Extensions;
using Microsoft.AspNetCore.JsonPatch;
using Microsoft.Extensions.Hosting;
using Serilog;
using CustomResourceDescriptorController.utils;

namespace CustomResourceDescriptorController.HostedServices
{
public class V1Alpha1Controller : IHostedService
{
private readonly IKubernetes mKubernetes;
private readonly IKeyManagement mKeyManagement;
private IDisposable mSubscription;
private readonly ILogger mAuditLogger = Log.ForContext<V1Alpha1Controller>().AsAudit();
private readonly ILogger mLogger = Log.ForContext<V1Alpha1Controller>();

public V1Alpha1Controller(IKubernetes kubernetes, IKeyManagement keyManagement)
{
this.mKubernetes = kubernetes;
this.mKeyManagement = keyManagement;
}

public Task StopAsync(CancellationToken cancellationToken)
{
if (mSubscription != null)
{
mSubscription.Dispose();
}
return Task.CompletedTask;
}

public Task StartAsync(CancellationToken token)
{
mSubscription =
mKubernetes.ObserveClusterCustomObject<KamusSecret>(
"soluto.com",
"v1alpha1",
"kamussecrets",
token)
.SelectMany(x =>
Observable.FromAsync(async () => await HandleEvent(x.Item1, x.Item2))
)
.Subscribe(
onNext: t => { },
onError: e =>
{
mLogger.Error(e, "Unexpected error occured while watching KamusSecret events");
Environment.Exit(1);
},
onCompleted: () =>
{
mLogger.Information("Watching KamusSecret events completed, terminating process");
Environment.Exit(0);
});

mLogger.Information("Starting watch for KamusSecret V1Alpha1 events");

return Task.CompletedTask;
}

private async Task HandleEvent(WatchEventType @event, KamusSecret kamusSecret)
{
try
{
mLogger.Information("Handling event of type {type}. KamusSecret {name} in namespace {namespace}",
@event.ToString(),
kamusSecret.Metadata.Name,
kamusSecret.Metadata.NamespaceProperty ?? "default");

switch (@event)
{
case WatchEventType.Added:
await HandleAdd(kamusSecret);
return;

case WatchEventType.Deleted:
await HandleDelete(kamusSecret);
return;

case WatchEventType.Modified:
await HandleModify(kamusSecret);
return;
default:
mLogger.Warning(
"Event of type {type} is not supported. KamusSecret {name} in namespace {namespace}",
@event.ToString(),
kamusSecret.Metadata.Name,
kamusSecret.Metadata.NamespaceProperty ?? "default");
return;

}
}
catch (Exception e)
{
mLogger.Error(e,
"Error while handling KamusSecret event of type {eventType}, for KamusSecret {name} on namespace {namespace}",
@event.ToString(),
kamusSecret.Metadata.Name,
kamusSecret.Metadata.NamespaceProperty ?? "default");
}
}

private async Task<V1Secret> CreateSecret(KamusSecret kamusSecret)
{
var @namespace = kamusSecret.Metadata.NamespaceProperty ?? "default";
var serviceAccount = kamusSecret.ServiceAccount;
var id = $"{@namespace}:{serviceAccount}";

mLogger.Debug("Starting decrypting KamusSecret items. KamusSecret {name} in namespace {namespace}",
kamusSecret.Metadata.Name,
@namespace);

Action<Exception, string> errorHandler = (e, key) => mLogger.Error(e,
"Failed to decrypt KamusSecret key {key}. KamusSecret {name} in namespace {namespace}",
key,
kamusSecret.Metadata.Name,
@namespace);

var decryptedStrings = await mKeyManagement.DecryptItems(kamusSecret.Data, id, errorHandler, x => x);

mLogger.Debug("KamusSecret items decrypted successfully. KamusSecret {name} in namespace {namespace}",
kamusSecret.Metadata.Name,
@namespace);

return new V1Secret
{
Metadata = new V1ObjectMeta
{
Name = kamusSecret.Metadata.Name,
NamespaceProperty = @namespace
},
Type = kamusSecret.Type,
StringData = decryptedStrings
};
}

private async Task HandleAdd(KamusSecret kamusSecret, bool isUpdate = false)
{
var secret = await CreateSecret(kamusSecret);
var createdSecret =
await mKubernetes.CreateNamespacedSecretAsync(secret, secret.Metadata.NamespaceProperty);

mAuditLogger.Information("Created a secret from KamusSecret {name} in namespace {namespace} successfully.",
kamusSecret.Metadata.Name,
secret.Metadata.NamespaceProperty);
}

private async Task HandleModify(KamusSecret kamusSecret)
{
var secret = await CreateSecret(kamusSecret);
var secretPatch = new JsonPatchDocument<V1Secret>();
secretPatch.Replace(e => e.StringData, secret.StringData);
var createdSecret = await mKubernetes.PatchNamespacedSecretAsync(
new V1Patch(secretPatch),
kamusSecret.Metadata.Name,
secret.Metadata.NamespaceProperty
);

mAuditLogger.Information("Updated a secret from KamusSecret {name} in namespace {namespace} successfully.",
kamusSecret.Metadata.Name,
secret.Metadata.NamespaceProperty);
}

private async Task HandleDelete(KamusSecret kamusSecret)
{
var @namespace = kamusSecret.Metadata.NamespaceProperty ?? "default";

await mKubernetes.DeleteNamespacedSecretAsync(kamusSecret.Metadata.Name, @namespace);
}
}
}
Loading

0 comments on commit 5c7be1c

Please sign in to comment.