From bed7bcf6f3703cd98bc78558184dc889dbe80b87 Mon Sep 17 00:00:00 2001 From: Roberto Alsina Date: Mon, 1 Jul 2024 10:16:48 -0300 Subject: [PATCH] Secrets REST API --- .ameba.yml | 5 +++-- docs/secrets.md | 34 ++++++++++++++++++++++++++++++--- src/daemon-secrets.cr | 44 +++++++++++++++++++++++++++++++++++++++++++ src/daemon.cr | 3 ++- 4 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 src/daemon-secrets.cr diff --git a/.ameba.yml b/.ameba.yml index 1de02d4..4f52602 100644 --- a/.ameba.yml +++ b/.ameba.yml @@ -1,9 +1,9 @@ # This configuration file was generated by `ameba --gen-config` -# on 2024-06-30 18:10:12 UTC using Ameba version 1.6.1. +# on 2024-07-01 13:11:14 UTC using Ameba version 1.6.1. # The point is for the user to remove these configuration records # one by one as the reported problems are removed from the code base. -# Problems found: 10 +# Problems found: 11 # Run `ameba --only Documentation/DocumentationAdmonition` for details Documentation/DocumentationAdmonition: Description: Reports documentation admonitions @@ -12,6 +12,7 @@ Documentation/DocumentationAdmonition: - src/faaso.cr - src/daemon.cr - src/funko.cr + - src/daemon-secrets.cr - spec/faaso_spec.cr Admonitions: - TODO diff --git a/docs/secrets.md b/docs/secrets.md index 86b82eb..655a7cf 100644 --- a/docs/secrets.md +++ b/docs/secrets.md @@ -6,10 +6,10 @@ However, code running in docker containers like a funko does often needs access to secrets, such as passwords to databases. Let's use that as the example secret for the rest of the document. -Also, not all funkos should have access to all the secrets, and the +Also, not all funkos should have access to all the secrets, and the "need to know" should be declarative in the funko's metadata. -## Problem 1: accessing secrets in the proxy container from the funkos +## Problem 1: accessing secrets in the proxy container from the funkos Let's further assume that faaso-proxy has access *somehow* to all the secrets, identified by a name. So there is a "dbpass" secret that has "verysecret" as @@ -18,4 +18,32 @@ it's very secret content. Also let's assume the proxy has access to a folder in the server filesystem, `/secrets` via something like a bind mount. -## Problem 2: how can the proxy know the secrets without keeping them in the image? \ No newline at end of file +If the proxy has access to the funko metadata, it can access a declaration of +what secrets the funko needs. + +Alternatively ... convention! + +Secrets for the funko foo are called foo-{name}, so our example is called foo-dbpass. + +So, the proxy can periodically examine its secret store and populate a folder +`/secrets/foo` with a `dbpass` file containing "verysecret". + +If on starting a funko we always do a bind mount of `/secrets/foo` to `/secrets` +then it will always have its secrets in place. + +## Problem 2: how can the proxy know the secrets without keeping them in the image? + +They can't be shipped via the image, so they need to be injected via the admin API. + +Let's give it a /secret endpoint and have all the usual REST stuff there. + +## The Good + +* It should work +* No secrets are unencrypted at rest in images + +## The Bad + +* Secrets are unencrypted at rest in the server filesystem +* Secrets are only sort-of-persistent? If the proxy is restarted, it will need + the secrets reinjected, or we need a persistent secret store in the server filesystem. diff --git a/src/daemon-secrets.cr b/src/daemon-secrets.cr new file mode 100644 index 0000000..39f93d0 --- /dev/null +++ b/src/daemon-secrets.cr @@ -0,0 +1,44 @@ +require "kemal" + +SECRETS = Hash(String, String).new +SECRET_PATH = "./secrets/" + +# TODO: sanitize all inputs + +# Store secrets in a tree of files +def update_secrets + # Save new secrets + SECRETS.map do |_name, value| + funko, name = _name.split("-", 2) + funko_dir = Path.new(SECRET_PATH, funko) + Dir.mkdir_p(funko_dir) + File.write(Path.new(funko_dir, name), value) + end + # Delete secrets not in the hash + Dir.glob(Path.new(SECRET_PATH, "*")).each do |funko_dir| + funko = File.basename(funko_dir) + Dir.glob(Path.new(funko_dir, "*")).each do |secret_file| + name = File.basename(secret_file) + unless SECRETS.has_key?("#{funko}-#{name}") + File.delete(secret_file) + end + end + end +end + +# Gets a secret in form {"name": "funko_name-secret_name", "value": "secret_value"} +post "/secrets/" do |env| + name = env.params.json["name"].as(String) + value = env.params.json["value"].as(String) + SECRETS[name] = value + update_secrets + halt env, status_code: 201, response: "Created" +end + +# Deletes a secret from the disk and memory +delete "/secrets/:name/" do |env| + name = env.params.url["name"] + SECRETS.delete(name) + update_secrets + halt env, status_code: 204, response: "Deleted" +end diff --git a/src/daemon.cr b/src/daemon.cr index fa90381..e4ca06e 100644 --- a/src/daemon.cr +++ b/src/daemon.cr @@ -1,5 +1,6 @@ -require "crystar" +require "./daemon-secrets.cr" require "compress/gzip" +require "crystar" require "docr" require "kemal-basic-auth" require "kemal"