EyesOnly

EyesOnly

Secrets are usually involuntary leaked by developers. Sometimes they are sent to external services like or written in a log by accident. Would not be useful to have a tool to limit the access to secrets? Enter EyesOnly.

EyesOnly: a package to limit access to secrets

Dealing last years with containerazing applications has left me thinking in how we are passing some secrets as environment variables to the container. This is standard practice by the Cloud Native practitioners. However, once you pass an environment variable you fall in two different issues:

  • You have global data.
  • Data can be secret and involuntary leaked.

The first one can be ignored by now, the second one cannot be. A leak can be potentially dangerous for a company, e.g. the logs are usually accessed for more people than production servers.

But, what is a leak?

In old times before the container revolution, a leak was simply publishing a secret in GitHub or whatever repository platform. With the advent of Docker and all other containers, and above all with the coming of the Cloud Native Principles, the leaking of secrets has a different meaning.

As the containers must be isolated/immutable and state-less, configuration and secrets are usually passed as environment variables. The software is prepared to read from the environment and do whatever it has to do, connect to an external API, access to a database, download some packages from a private code repository, etc.

The problem now is that logging and exception capturing services can inadvertedly print these secrets and, then discovered by a big audience.

How to solve it?

Encryption of secrets

First solution would be by encrypting the secrets with a temporal key. That way, if there is a leak in a log file, as long as you don’t have access to the encrypting key the secret is safe.

But you have to deal with the process of encryption and decryption, you also need to deal with another additional secret, and besides that you have no guarantee of no leaks if a developer decrypts the secret and uses it uncarefully.

EyesOnly: Secret Access Control

The other solution I propose here is control what functions access the secrets: EyesOnly is a prototype I have just released to help developers control the access to their secrets.

Of course it is not a infallible solution: think of a function that is allowed to access a secret but it writes the secret in a log. This Python package provides a simple way of doing access control to the secrets. How you deal with the secrets in the allowed functions is your matter.

EyesOnly is very easy to use and accept JSON and toml configurations:

JSON configuration

Creating a JSON configuration is the best and easiest way of configure EyesOnly:

{
  "eyesonly":{
    "secrets": [
      {
        "secret": "geo_api_key",
        "files": [
          {
            "file_path": "../../secrets_use.py",
            "functions": [
              "allowed_use1",
              "allowed_use2",
              "allowed_use3"
            ]
          },
          {
            "file_path": "../../another_secrets_use.py",
            "functions": [
              "anther_allowed_use1"
            ]
          }
        ]
      },
      {
        "secret": "postgresql_password",
        "files": [
          {
            "file_path": "../../secrets_use.py",
            "functions": ["allowed_use1"]
          }
        ]
      }
    ]
  }
}

Notice that in the case of passing the JSON inside a file, the file paths can be absolute or relative with respect to the path of the file itself, but if we decide to pass the JSON in an environment variable, all paths must be absolute paths.

There is also the option to use a toml file:

[eyesonly]
[[eyesonly.secrets]]
secret = 'secret1'
[[eyesonly.secrets.files]]
file_path = '../../path/to/secret11.py'
functions = [
    'func1a',
    'func1b'
]
[[eyesonly.secrets.files]]
file_path = '../../path/to/secret12.py'
functions = [
    'func2a',
    'func2b'
]

[[eyesonly.secrets]]
secret = 'secret2'
[[eyesonly.secrets.files]]
file_path = '/root/path/to/secret2.py'
functions =[
    'func3',
    'func4'
]

I prefer using JSON, but you are free to use this file format if you wish.

Loading the configuration

from eyesonly.secret import Secret
from eyesonly.acl.acl import ACL
from eyesonly.acl.providers.json_acl_provider import JSONACLProvider
from eyesonly.acl.providers.env_acl_provider import EnvACLProvider
from eyesonly.acl.providers.toml_acl_provider import TomlACLProvider

# JSON configuration file
json_acl = ACL(JSONACLProvider(file_path='path/of/your/json/config/file'))

# JSON in environment variable
env_acl = ACL(EnvACLProvider(env_variable='variable'))

# toml configuration file
toml_acl = ACL(TomlACLProvider(file_path='path/of/your/toml/config/file'))

We have to load the access configuration, and then we can use it to read the secrets from the environment or whatever source we would like:

Secret loading

# secret_depository.py
import os
from eyesonly.secret import Secret

GEO_API_SECRET = Secret(name='geo_api_key', value=os.environ['GEO_SERVICE_API_KEY'], acl=json_acl)
DB_PASSWORD = Secret(name='postgresql_password', value=os.environ['DB_PASSWORD'], acl=json_acl, denied_policy='censure')

I recommend to centralize all secret management in a module or class. This is a good way of avoiding scatterring of secret management.

Another thing I have to mention, is the ability to set a policy of what to do in case of an unlawfull access. By default, an exception is raised, but if you are content when printing a string of asterisks, you can set the denied_policy='censure' and your code will continue working, but '*****' will be shown.

Use of secrets

Convert the Secret objects to strings when you need to use the secret. As long as you are in an allowed function, you will be able to do it.

# secrets_use.py
import os
from .secret_depository import GEO_API_SECRET, DB_PASSWORD
from eyesonly.secret import Secret


def allowed_use1():
    # Both secrets can be seen in this function 
    assert os.environ['GEO_SERVICE_API_KEY'] == str(GEO_API_SECRET)
    assert os.environ['DB_PASSWORD'] == str(DB_PASSWORD)


def allowed_use2():
    # geo_api_key can be seen in this function 
    return str(GEO_API_SECRET)
    

def another_use3():
    # geo_api_key can be seen in this function 
    assert os.environ['GEO_SERVICE_API_KEY'] == allowed_use2()


def geo_api_key_not_allowed():
    # geo_api_key can NOT be seen in this function and will raise an exception
    return str(GEO_API_SECRET)


def postgresql_password_not_allowed():
    # postgresql_password can NOT be seen in this function but will return
    # a string with only asterisks because of the "denied_policy" parameter
    # in the Secret initializer.
    assert '*****' == str(DB_PASSWORD)

Future work

EyesOnly is not a fully completed project (as most software projects are).

  • An encrypting/decrypting phase could be added.
  • A way of removing inheritance of secret access permission.
  • Your needed feature Reddit thread.
  • etc.

Conclusion

I have shown a tool to help you deal with secrets: EyesOnly. It can help you reduce secret leaks in your code by controlling what functions can access which secret.

Discuss this post in reddit.