MatrixAI / Polykey

Polykey Core Library
https://polykey.com
GNU General Public License v3.0
29 stars 4 forks source link

Integration with OS Keychains #201

Open CMCDragonkai opened 3 years ago

CMCDragonkai commented 3 years ago

Is your feature request related to a problem? Please describe.

Applications often delegate credential storage to a third party program on the OS rather than writing it themselves. Since Polykey is not a well-known credential storage, then PK won't immediately be used as a credential storage system. Therefore this limits the UX of using Polykey. It's an integration problem.

Browsers maintain their own credentials, and every other system has their own as well.

These third party programs include:

For example Docker is capable of integrating into these keychains as you can see here: https://github.com/docker/docker-credential-helpers

By default, Docker looks for the native binary on each of the platforms, i.e. “osxkeychain” on macOS, “wincred” on windows, and “pass” on Linux. A special case is that on Linux, Docker will fall back to the “secretservice” binary if it cannot find the “pass” binary. If none of these binaries are present, it stores the credentials (i.e. password) in base64 encoding in the config files described above. https://docs.docker.com/engine/reference/commandline/login/#credentials-store

This shows you that Docker login credentials are not even encrypted at rest.

Android and iOS are also important here. In fact Android and iOS integration I believe actually supersedes browser integration. So you don't really create a "browser plugin" for the mobile phone browsers, but a password manager can be integrated to the OS. For example: https://developer.android.com/guide/topics/text/autofill and https://developer.apple.com/documentation/security/password_autofill/

Describe the solution you'd like

It would be nice for Polykey to capable of performing secret injection into the OS keychain to use these as potential integration points.

Describe alternatives you've considered

A generic integration point on desktop OSes is using the clipboard or performing autofill.

For autofill/autotype on desktop, we should look at this:

I think desktops don't have a direct autofill/autotype feature builtin unlike mobile systems. But this would be quite useful for desktop applications.

Additional context

CMCDragonkai commented 3 years ago

This is new feature request, and it requires review into our product vision, and if accepted, we are meant to create a design issue for this.

CMCDragonkai commented 6 months ago

Some recent experiments on running a grafana-agent due to #599 was interesting in showcasing how systemd works to manage credentials especially on NixOS.

This has some relation with #169.

The grafana-agent service config looks like this in NixOS:

{
  enable = true;
  settings = {
    integrations = {
      prometheus_remote_write = [
        {
          basic_auth = {
            username = "\${METRICS_USERNAME}";
            password = "\${METRICS_PASSWORD}";
          };
          url = "\${METRICS_URL}";
        }
      ];
    };
    logs = {
      configs = [
        {
          clients = [
            {
              basic_auth = {
                username = "\${LOGS_USERNAME}";
                password = "\${LOGS_PASSWORD}";
              };
              url = "\${LOGS_URL}";
            }
          ];
        }
      ];
    };
    metrics = {
      configs = [
        {
          name = "integrations";
          remote_write = [
            {
              basic_auth = {
                username = "\${METRICS_USERNAME}";
                password = "\${METRICS_PASSWORD}";
              };
              url = "\${METRICS_URL}";
            }
          ];
        }
      ];
      wal_directory = "\${STATE_DIRECTORY}";
    };
  };
  credentials = {
    METRICS_URL = "/run/keys/grafana_agent/metrics_url";
    METRICS_USERNAME = "/run/keys/grafana_agent/metrics_username";
    METRICS_PASSWORD = "/run/keys/grafana_agent/metrics_password";
    LOGS_URL = "/run/keys/grafana_agent/logs_url";
    LOGS_USERNAME = "/run/keys/grafana_agent/logs_username";
    LOGS_PASSWORD = "/run/keys/grafana_agent/logs_password";
  };
}

What's interesting here is that the credentials specially managed by the Nix derivation for the systemd unit.

Here you can see that they are key values where the value is a path to a file. The file path is in /run/keys which is supposed to be owned by the root user. By keeping as files instead of directly embedding them in the config, we avoid loading the credentials directly into the evaluated Nix which would be globally readable in the /nix/store.

This is then used here: https://github.com/NixOS/nixpkgs/blob/3dc440faeee9e889fe2d1b4d25ad0f430d449356/nixos/modules/services/monitoring/grafana-agent.nix#L127C1-L161C7

The 2 key things are:

LoadCredential = lib.mapAttrsToList (key: value: "${key}:${value}") cfg.credentials;

Here you can see how Nix is used as a templating language for arbitrary configuration. This systemd directive basically reads the file paths and puts them into a special location called the CREDENTIALS_DIRECTORY.

This is all documented here: https://systemd.io/CREDENTIALS/

Later inside the script that actually gets executed, it does this:

# Load all credentials into env if they are in UPPER_SNAKE form.
if [[ -n "${CREDENTIALS_DIRECTORY:-}" ]]; then
  for file in "$CREDENTIALS_DIRECTORY"/*; do
    key=$(basename "$file")
    if [[ $key =~ ^[A-Z0-9_]+$ ]]; then
      echo "Environ $key"
      export "$key=$(< "$file")"
    fi
  done
fi

Thus ensuring that all the files under CREDENTIALS_DIRECTORY gets loaded as an environment variable.

By default systemd doesn't do this, because environment variables are not as isolated as files as per:

Access to credentials is restricted to the service’s user. Unlike environment variables the credential data is not propagated down the process tree. Instead each time a credential is accessed an access check is enforced by the kernel. If the service is using file system namespacing the loaded credential data is invisible to all other services.

Basically this is the problem of the confused deputy. If there other processes whether they are sibling processes or child processes with the same ambient environment, they may be able to access those credentials by looking at the environment of a given process. File systems then have more fine grained access control that can be applied to a given situation. But with script it just gives some flexibility in using environment variables.


Note that I have some problem with it reloading credentials: https://github.com/systemd/systemd/issues/21099

This ends up requiring a restart of the service atm.