Secrets REST API

This commit is contained in:
Roberto Alsina 2024-07-01 10:16:48 -03:00
parent ae328f465c
commit bed7bcf6f3
4 changed files with 80 additions and 6 deletions

View File

@ -1,9 +1,9 @@
# This configuration file was generated by `ameba --gen-config` # 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 # The point is for the user to remove these configuration records
# one by one as the reported problems are removed from the code base. # 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 # Run `ameba --only Documentation/DocumentationAdmonition` for details
Documentation/DocumentationAdmonition: Documentation/DocumentationAdmonition:
Description: Reports documentation admonitions Description: Reports documentation admonitions
@ -12,6 +12,7 @@ Documentation/DocumentationAdmonition:
- src/faaso.cr - src/faaso.cr
- src/daemon.cr - src/daemon.cr
- src/funko.cr - src/funko.cr
- src/daemon-secrets.cr
- spec/faaso_spec.cr - spec/faaso_spec.cr
Admonitions: Admonitions:
- TODO - TODO

View File

@ -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 access to secrets, such as passwords to databases. Let's use that as the
example secret for the rest of the document. 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. "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, 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 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, Also let's assume the proxy has access to a folder in the server filesystem,
`/secrets` via something like a bind mount. `/secrets` via something like a bind mount.
## Problem 2: how can the proxy know the secrets without keeping them in the image? 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.

44
src/daemon-secrets.cr Normal file
View File

@ -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

View File

@ -1,5 +1,6 @@
require "crystar" require "./daemon-secrets.cr"
require "compress/gzip" require "compress/gzip"
require "crystar"
require "docr" require "docr"
require "kemal-basic-auth" require "kemal-basic-auth"
require "kemal" require "kemal"