class Amber::Configuration::CredentialsDirectory

Overview

Reads a credentials directory: one file per credential, where the file name is the credential name and the file contents are its value.

This is the layout systemd's LoadCredential= and LoadCredentialEncrypted= produce in $CREDENTIALS_DIRECTORY. The reader takes the directory path as an argument and reads files only; the application's configuration decides where the path comes from.

Contents are kept byte for byte, including any trailing newline, so write credential files without one (printf '%s' "$secret").

Each read opens the directory once and then lists, checks and opens every entry relative to that open directory, so replacing the directory or one of its parents with a symlink part way through cannot redirect the read. Each credential file is opened without following a final symlink and without blocking, then checked through the open descriptor, so an entry swapped for a symlink or a FIFO after it was listed is rejected rather than read or waited on. Reads are capped per file and per read. Windows has no credentials directory, and the reader raises Amber::Exceptions::CredentialsDirectoryUnsupported there.

Every failure raises a named Amber::Exceptions::CredentialsError subclass whose message names the directory and the credential, never its value. The reader does not log.

struct AppCredentials
  include YAML::Serializable

  property secret_key_base : String = ""
  property database_url : String? = nil

  def initialize
  end
end

directory = Amber::Configuration::CredentialsDirectory.new("/run/credentials/app.service")
directory.read_credential("secret_key_base") # => "..."
directory.read                               # => {"database_url" => "...", "secret_key_base" => "..."}
directory.read_into(AppCredentials)          # => AppCredentials(@secret_key_base="...", ...)

Defined in:

amber/configuration/credentials_directory.cr

Constant Summary

DEFAULT_MAXIMUM_FILE_SIZE = 64_i64 * 1024

The default cap on a single credential file: 64 KiB.

DEFAULT_MAXIMUM_TOTAL_SIZE = (1_i64 * 1024) * 1024

The default cap on everything one read takes from the directory, counting file names and contents: 1 MiB, systemd's own total limit.

LARGEST_ALLOWED_SIZE = Int32::MAX.to_i64

The largest cap either size limit accepts.

MAXIMUM_LINK_TARGET_SIZE = 4096

The longest symlink target the reader looks at (PATH_MAX).

MAXIMUM_NAME_SIZE = 255

The longest credential name a file system accepts (NAME_MAX).

Constructors

Class Method Summary

Instance Method Summary

Constructor Detail

def self.new(directory : String, maximum_file_size : Int64 = DEFAULT_MAXIMUM_FILE_SIZE, maximum_total_size : Int64 = DEFAULT_MAXIMUM_TOTAL_SIZE) #

Raises ArgumentError when a cap is negative or above LARGEST_ALLOWED_SIZE.


[View source]

Class Method Detail

def self.mapping_of(values : Hash(String, String)) : YAML::Nodes::Mapping #

Builds a YAML mapping of double-quoted scalars from credential values, so a credential whose contents are null or true stays that text in a String? field. :nodoc:


[View source]
def self.section_from(type : T.class, values : Hash(String, String), directory : String) : T forall T #

Builds a section of type from credential values keyed by YAML key. :nodoc:


[View source]

Instance Method Detail

def directory : String #

[View source]
def maximum_file_size : Int64 #

[View source]
def maximum_total_size : Int64 #

[View source]
def read : Hash(String, String) #

Reads every credential in the directory into a hash keyed by file name.

Raises Amber::Exceptions::CredentialsDirectoryTooLarge when the file names and contents together exceed #maximum_total_size.


[View source]
def read_credential(name : String) : String #

Reads one credential by name.

Raises Amber::Exceptions::CredentialPathTraversal when name is not a plain file name, such as ../other_service/key, Amber::Exceptions::CredentialNotFound when no file has that name, and Amber::Exceptions::CredentialsDirectoryTooLarge when the name and contents exceed #maximum_total_size.


[View source]
def read_credential?(name : String) : String | Nil #

Reads one credential by name, or returns nil when no file has that name.


[View source]
def read_for(type : T.class) : Hash(String, String) forall T #

Reads the credentials that type declares: one file per YAML key of the type, skipping keys with no file. Other files in the directory are neither read nor checked.

Raises Amber::Exceptions::CredentialPathTraversal when the type declares a key that is not a plain file name.


[View source]
def read_into(type : T.class) : T forall T #

Reads the credentials that type declares and builds the typed section.

Each file name maps to the field with that YAML key. Files with no matching field are ignored; fields with no file keep their default.


[View source]