Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vault abstraction for Galaxy #12940

Merged
merged 29 commits into from
Dec 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
43fbc36
Add interface definitions and test case for abstract vault
nuwang Nov 17, 2021
1a26b45
Add initial implementation and tests for hashicorp vault
nuwang Nov 17, 2021
f207b23
Add database based vault implementation
nuwang Nov 17, 2021
e1535db
Add vault conditional dependencies
nuwang Nov 17, 2021
a05f3c2
Add test for secret overwrites
nuwang Nov 18, 2021
5fcfb00
Add initial custos vault backend
nuwang Nov 18, 2021
4695dc8
Further simplify vault interface to be string key value pairs
nuwang Nov 18, 2021
bf248f7
Rename VaultFactory.from_app_config to from_app
nuwang Nov 20, 2021
e6494b1
Added support for storing extra preferences in the vault + tests
nuwang Nov 21, 2021
0572f62
Fix vault linting errors
nuwang Nov 21, 2021
062618b
Fix typing errors in vault
nuwang Nov 23, 2021
569db6b
Make vault tests conditional
nuwang Nov 24, 2021
76951a4
Make sure filesources can access user level and app level vaults
nuwang Nov 28, 2021
d04b56b
Added vault model mapping test and some minor refactoring
nuwang Nov 28, 2021
07779aa
Use GalaxyDataTestApp in Vault tests instead of MockApp
nuwang Nov 28, 2021
8c6c1b6
Validate vault keys
nuwang Dec 1, 2021
2bc7bb4
Added validation and prefixing decorators to vault
nuwang Dec 1, 2021
9b64e30
Store parent key in database vault for quicker lookups
nuwang Dec 1, 2021
7f663fa
Add new secret type to user preferences that is not sent to client
nuwang Dec 1, 2021
85b6f63
Fix vault table parent reference
nuwang Dec 1, 2021
d9f2c0b
Added admin docs on configuring and using the vault
nuwang Dec 1, 2021
7328ca0
Renamed vault migration script and some minor test fixes
nuwang Dec 1, 2021
1415bad
Update vault to support latest custos sdk changes
nuwang Dec 2, 2021
80bd6e8
Rename vault classes and some docs
nuwang Dec 12, 2021
3e99366
Typo in doc/source/admin/special_topics/vault.md
nuwang Dec 15, 2021
592603e
Add file upload test to vault
nuwang Dec 20, 2021
be9f210
Change vault api test case to integration test case and add missing c…
nuwang Dec 20, 2021
d4fe61a
Update vault docs
nuwang Dec 20, 2021
01ca66f
Merge branch 'dev' into vault_impl
nuwang Dec 22, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion doc/source/admin/galaxy_options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4781,5 +4781,13 @@
:Default: ``plugins/welcome_page/new_user/static/topics/``
:Type: str

~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``vault_config_file``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~


:Description:
Vault configuration
The value of this option will be resolved with respect to
<config_dir>.
:Default: ``vault_conf.yml``
:Type: str
1 change: 1 addition & 0 deletions doc/source/admin/special_topics/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Special Topics
gtn
job_metrics
webhooks
vault
performance_tracking
bug_reports
gdpr_compliance
124 changes: 124 additions & 0 deletions doc/source/admin/special_topics/vault.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Storing secrets in the vault

Galaxy can be configured to store secrets in an external vault, which is useful for secure handling and centralization of secrets management.
In particular, information fields in the "Manage information" section of the user profile, such as AWS credentials, can be configured to be stored
in an encrypted vault instead of the database. Vault keys are generally stored as key-value pairs in a hierarchical fashion, for example:
`/galaxy/user/2/preferences/aws/client_secret`.

## Vault backends

There are currently 3 supported backends.

| Backend | Description |
|-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| hashicorp | Hashicorp Vault is a secrets and encryption management system. https://www.vaultproject.io/ |
| custos | Custos is an NSF-funded project, backed by open source software that provides science gateways such as Galaxy with single sign-on, group management, and management of secrets such as access keys and OAuth2 access tokens. Custos secrets management is backed by Hashicorp's vault, but provides a convenient, always-on ReST API service. |
| database | The database backend stores secrets in an encrypted table in the Galaxy database itself. It is a convenient way to get started with a vault, and while it supports basic key rotation, we recommend using one of the other options in production. |

## Configuring Galaxy

The first step to using a vault is to configure the `vault_config_file` setting in `galaxy.yml`.

```yaml
galaxy:
# Vault config file
# The value of this option will be resolved with respect to
# <config_dir>.
vault_config_file: vault_conf.yml
```

## Configuring vault_conf.yml

The vault_conf.yml file itself has two basic fields:
```yaml
type: hashicorp # required
nuwang marked this conversation as resolved.
Show resolved Hide resolved
path_prefix: /galaxy # optional
...
```

The `type` must be a valid backend type: `hashicorp`, `custos`, or `database`. At present, only a single vault backend
is supported. The `path_prefix` property indicates the root path under which to store all vault keys. If multiple
Galaxy instances are using the same vault, a prefix can be used to uniquely identify the Galaxy instance.
If no path_prefix is provided, the prefix defaults to `/galaxy`.

## Vault configuration for Hashicorp Vault

```yaml
type: hashicorp
path_prefix: /my_galaxy_instance
vault_address: http://localhost:8200
vault_token: vault_application_token
```

nuwang marked this conversation as resolved.
Show resolved Hide resolved
## Vault configuration for Custos

```yaml
type: custos
custos_host: service.staging.usecustos.org
custos_port: 30170
custos_client_id: custos-jeREDACTEDye-10000001
custos_client_sec: OGREDACTEDBSUDHn
```

nuwang marked this conversation as resolved.
Show resolved Hide resolved
Obtaining the Custos client id and client secret requires first registering your Galaxy instance with Custos.
Visit [usecustos.org](http://usecustos.org/) for more information.

## Vault configuration for database

```yaml
type: database
path_prefix: /galaxy
# Encryption keys must be valid fernet keys
# To generate a valid key:
#
# Use the ascii string value as a key
# For more details, see: https://cryptography.io/en/latest/fernet/#
encryption_keys:
- 5RrT94ji178vQwha7TAmEix7DojtsLlxVz8Ef17KWgg=
- iNdXd7tRjLnSqRHxuhqQ98GTLU8HUbd5_Xx38iF8nZ0=
- IK83IXhE4_7W7xCFEtD9op0BAs11pJqYN236Spppp7g=
```

Secrets stored in the database are encrypted using Fernet keys. Therefore, all listed encryption keys must be valid fernet keys. To generate a new
Fernet key, use the following Python code:

```python
from cryptography.fernet import Fernet
Fernet.generate_key().decode('utf-8')

If multiple encryption keys are defined, only the first key is used to encrypt secrets. The remaining keys are tried in turn during decryption. This is useful for key rotation.
We recommend periodically generating a new fernet key and rotating old keys. However, before removing an old key, make sure that any data encrypted using that old key is no longer
present or the data will no longer be decryptable.

## Configuring user preferences to use the vault

The `user_preferences_extra_conf.yml` can be used to automatically route secrets to a vault. An example configuration follows:

```yaml
preferences:
googledrive:
description: Your Google Drive account
inputs:
- name: client_id
label: Client ID
type: text
required: True
- name: client_secret
label: Client Secret
type: secret
store: vault
required: True
- name: access_token
label: Access token
type: password
store: vault
required: True
- name: refresh_token
label: Refresh Token
type: secret
store: vault
required: True
```

Note the `store: vault` property, which results in the property being stored in the vault. Note also that if you use `type: password`, the secret is sent to the client front-end,
but specifying `type: secret` would mean that the values cannot be retrieved by the client, only written to, providing an extra layer of security.
6 changes: 6 additions & 0 deletions lib/galaxy/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
)
from galaxy.quota import get_quota_agent, QuotaAgent
from galaxy.security.idencoding import IdEncodingHelper
from galaxy.security.vault import (
Vault,
VaultFactory
)
from galaxy.tool_shed.galaxy_install.installed_repository_manager import InstalledRepositoryManager
from galaxy.tool_shed.galaxy_install.update_repository_manager import UpdateRepositoryManager
from galaxy.tool_util.deps.views import DependencyResolversView
Expand Down Expand Up @@ -207,6 +211,8 @@ def __init__(self, **kwargs):
# ConfiguredFileSources
self.file_sources = self._register_singleton(ConfiguredFileSources, ConfiguredFileSources.from_app_config(self.config))

self.vault = self._register_singleton(Vault, VaultFactory.from_app(self))

# We need the datatype registry for running certain tasks that modify HDAs, and to build the registry we need
# to setup the installed repositories ... this is not ideal
self._configure_tool_config_files()
Expand Down
1 change: 1 addition & 0 deletions lib/galaxy/app_unittest_utils/galaxy_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ def __init__(self, **kwargs):
self.enable_tool_shed_check = False
self.monitor_thread_join_timeout = 1
self.integrated_tool_panel_config = None
self.vault_config_file = kwargs.get('vault_config_file')

@property
def config_dict(self):
Expand Down
4 changes: 4 additions & 0 deletions lib/galaxy/config/sample/galaxy.yml.sample
Original file line number Diff line number Diff line change
Expand Up @@ -2332,3 +2332,7 @@ galaxy:
# is relative to galaxy/static
#welcome_directory: plugins/welcome_page/new_user/static/topics/

# Vault config file
# The value of this option will be resolved with respect to
# <config_dir>.
vault_config_file: vault_conf.yml
18 changes: 18 additions & 0 deletions lib/galaxy/dependencies/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def __init__(self, config_file, config=None):
self.container_interface_types = []
self.job_rule_modules = []
self.error_report_modules = []
self.vault_type = None
if config is None:
self.config = load_app_properties(config_file=self.config_file)
else:
Expand Down Expand Up @@ -151,6 +152,17 @@ def collect_types(from_dict):
file_sources_conf = []
self.file_sources = [c.get('type', None) for c in file_sources_conf]

# Parse vault config
vault_conf_yml = self.config.get(
"vault_config_file",
join(dirname(self.config_file), 'vault_conf.yml'))
if exists(vault_conf_yml):
with open(vault_conf_yml) as f:
vault_conf = yaml.safe_load(f)
else:
vault_conf = {}
self.vault_type = vault_conf.get('type', '').lower()

def get_conditional_requirements(self):
crfile = join(dirname(__file__), 'conditional-requirements.txt')
for req in pkg_resources.parse_requirements(open(crfile).readlines()):
Expand Down Expand Up @@ -273,6 +285,12 @@ def check_weasyprint(self):
# See notes in ./conditional-requirements.txt for more information.
return os.environ.get("GALAXY_DEPENDENCIES_INSTALL_WEASYPRINT") == "1"

def check_custos_sdk(self):
return 'custos' == self.vault_type

def check_hvac(self):
return 'hashicorp' == self.vault_type
nuwang marked this conversation as resolved.
Show resolved Hide resolved


def optional(config_file=None):
if not config_file:
Expand Down
4 changes: 4 additions & 0 deletions lib/galaxy/dependencies/conditional-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ fs-gcsfs # type: googlecloudstorage
fs-onedatafs # type: onedata
fs-basespace # type: basespace

# Vault backend
hvac
custos-sdk

# Chronos client
chronos-python==1.2.1

Expand Down
20 changes: 20 additions & 0 deletions lib/galaxy/files/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,18 @@ def is_admin(self):
"""Whether this user is an administrator."""
return self.trans.user_is_admin

@property
def user_vault(self):
"""User vault namespace"""
user_vault = self.trans.user_vault
return user_vault or defaultdict(lambda: None)

@property
def app_vault(self):
"""App vault namespace"""
vault = self.trans.app.vault
return vault or defaultdict(lambda: None)


class DictFileSourcesUserContext(FileSourceDictifiable):

Expand Down Expand Up @@ -315,3 +327,11 @@ def group_names(self):
@property
def is_admin(self):
return self._kwd.get("is_admin")

@property
def user_vault(self):
return self._kwd.get("user_vault")

@property
def app_vault(self):
return self._kwd.get("app_vault")
6 changes: 6 additions & 0 deletions lib/galaxy/managers/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
from galaxy.model.base import ModelMapping
from galaxy.model.scoped_session import galaxy_scoped_session
from galaxy.security.idencoding import IdEncodingHelper
from galaxy.security.vault import UserVaultWrapper
from galaxy.structured_app import MinimalManagerApp
from galaxy.util import bunch

Expand Down Expand Up @@ -197,6 +198,11 @@ class ProvidesUserContext(ProvidesAppContext):
def user(self):
"""Provide access to the user object."""

@property
def user_vault(self):
"""Provide access to a user's personal vault."""
return UserVaultWrapper(self.app.vault, self.user)

@property
def anonymous(self) -> bool:
return self.user is None
Expand Down
12 changes: 12 additions & 0 deletions lib/galaxy/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
from sqlalchemy.ext.orderinglist import ordering_list
from sqlalchemy.orm import (
aliased,
backref,
column_property,
deferred,
joinedload,
Expand Down Expand Up @@ -8479,6 +8480,17 @@ class LibraryDatasetCollectionAnnotationAssociation(Base, RepresentById):
user = relationship('User')


class Vault(Base):
__tablename__ = 'vault'

key = Column(Text, primary_key=True)
parent_key = Column(Text, ForeignKey(key), index=True, nullable=True)
children = relationship('Vault', backref=backref('parent', remote_side=[key]))
value = Column(Text, nullable=True)
create_time = Column(DateTime, default=now)
update_time = Column(DateTime, default=now, onupdate=now)


# Item rating classes.
class ItemRatingAssociation(Base):
__abstract__ = True
Expand Down
25 changes: 25 additions & 0 deletions lib/galaxy/model/migrate/versions/0180_add_vault_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import datetime

from sqlalchemy import Column, DateTime, ForeignKey, MetaData, Table, Text

now = datetime.datetime.utcnow
meta = MetaData()

vault = Table(
'vault', meta,
Column('key', Text, primary_key=True),
Column('parent_key', Text, ForeignKey('vault.key'), index=True, nullable=True),
Column('value', Text, nullable=True),
Column("create_time", DateTime, default=now),
Column("update_time", DateTime, default=now, onupdate=now)
)


def upgrade(migrate_engine):
meta.bind = migrate_engine
vault.create()


def downgrade(migrate_engine):
meta.bind = migrate_engine
vault.drop()
Loading