Use secret_id's with vault-kv relation
In order to tighten the security around access to secrets stored in a Vault KV secrets backend, generate a secret_id for each accessing unit, using a response wrapping token which is passed over the relation to the consuming application. The consuming application will then use this token out-of-band of Juju to retrieve the secret_id associated with the AppRole ID directly from Vault. Add a new action 'refresh-secrets' to force a renewal of secret_id's and associated one-shot retrieval tokens across a deployment. A token is only issued when a new approle is created or when a refresh is initiated via the 'refresh-secrets' action. Change-Id: I2cd173514377d65542ea4fa67ccf700ea4b6ab89
This commit is contained in:
		@@ -6,3 +6,5 @@ authorize-charm:
 | 
				
			|||||||
      description: Token to use to authorize charm
 | 
					      description: Token to use to authorize charm
 | 
				
			||||||
  required:
 | 
					  required:
 | 
				
			||||||
  - token
 | 
					  - token
 | 
				
			||||||
 | 
					refresh-secrets:
 | 
				
			||||||
 | 
					  description: Refresh secret_id's and re-issue retrieval tokens for secrets endpoints
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,6 +29,8 @@ import charm.vault as vault
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import charms.reactive
 | 
					import charms.reactive
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from charms.reactive.flags import set_flag
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def authorize_charm_action(*args):
 | 
					def authorize_charm_action(*args):
 | 
				
			||||||
    """Create a role allowing the charm to perform certain vault actions.
 | 
					    """Create a role allowing the charm to perform certain vault actions.
 | 
				
			||||||
@@ -39,10 +41,20 @@ def authorize_charm_action(*args):
 | 
				
			|||||||
    role_id = vault.setup_charm_vault_access(action_config['token'])
 | 
					    role_id = vault.setup_charm_vault_access(action_config['token'])
 | 
				
			||||||
    hookenv.leader_set({vault.CHARM_ACCESS_ROLE_ID: role_id})
 | 
					    hookenv.leader_set({vault.CHARM_ACCESS_ROLE_ID: role_id})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def refresh_secrets(*args):
 | 
				
			||||||
 | 
					    """Refresh secret_id's and re-issue tokens for secret_id retrieval
 | 
				
			||||||
 | 
					    on secrets end-points"""
 | 
				
			||||||
 | 
					    if not hookenv.is_leader():
 | 
				
			||||||
 | 
					        hookenv.action_fail('Please run action on lead unit')
 | 
				
			||||||
 | 
					    set_flag('secrets.refresh')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Actions to function mapping, to allow for illegal python action names that
 | 
					# Actions to function mapping, to allow for illegal python action names that
 | 
				
			||||||
# can map to a python function.
 | 
					# can map to a python function.
 | 
				
			||||||
ACTIONS = {
 | 
					ACTIONS = {
 | 
				
			||||||
    "authorize-charm": authorize_charm_action,
 | 
					    "authorize-charm": authorize_charm_action,
 | 
				
			||||||
 | 
					    "refresh-secrets": refresh_secrets,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								src/actions/refresh-secrets
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								src/actions/refresh-secrets
									
									
									
									
									
										Symbolic link
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					actions.py
 | 
				
			||||||
@@ -299,6 +299,22 @@ def configure_approle(client, name, cidr, policies):
 | 
				
			|||||||
        token_ttl='60s',
 | 
					        token_ttl='60s',
 | 
				
			||||||
        token_max_ttl='60s',
 | 
					        token_max_ttl='60s',
 | 
				
			||||||
        policies=policies,
 | 
					        policies=policies,
 | 
				
			||||||
        bind_secret_id='false',
 | 
					        bind_secret_id='true',
 | 
				
			||||||
        bound_cidr_list=cidr)
 | 
					        bound_cidr_list=cidr)
 | 
				
			||||||
    return client.get_role_id(name)
 | 
					    return client.get_role_id(name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def generate_role_secret_id(client, name, cidr):
 | 
				
			||||||
 | 
					    """Generate a new secret_id for an AppRole
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param client: Vault client
 | 
				
			||||||
 | 
					    :ptype client: hvac.Client
 | 
				
			||||||
 | 
					    :param name: Name of role
 | 
				
			||||||
 | 
					    :ptype name: str
 | 
				
			||||||
 | 
					    :param cidr: Network address of remote unit
 | 
				
			||||||
 | 
					    :ptype cidr: str
 | 
				
			||||||
 | 
					    :returns: Vault token to retrieve the response-wrapped response
 | 
				
			||||||
 | 
					    :rtype: str"""
 | 
				
			||||||
 | 
					    response = client.write('auth/approle/role/{}/secret-id'.format(name),
 | 
				
			||||||
 | 
					                            wrap_ttl='1h', cidr_list=cidr)
 | 
				
			||||||
 | 
					    return response['wrap_info']['token']
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -44,6 +44,7 @@ from charms.reactive import (
 | 
				
			|||||||
    when,
 | 
					    when,
 | 
				
			||||||
    when_file_changed,
 | 
					    when_file_changed,
 | 
				
			||||||
    when_not,
 | 
					    when_not,
 | 
				
			||||||
 | 
					    when_any,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from charms.reactive.relations import (
 | 
					from charms.reactive.relations import (
 | 
				
			||||||
@@ -370,7 +371,7 @@ def file_change_auto_unlock_mode():
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@when('leadership.is_leader')
 | 
					@when('leadership.is_leader')
 | 
				
			||||||
@when('endpoint.secrets.new-request')
 | 
					@when_any('endpoint.secrets.new-request', 'secrets.refresh')
 | 
				
			||||||
def configure_secrets_backend():
 | 
					def configure_secrets_backend():
 | 
				
			||||||
    """ Process requests for setup and access to simple kv secret backends """
 | 
					    """ Process requests for setup and access to simple kv secret backends """
 | 
				
			||||||
    @tenacity.retry(wait=tenacity.wait_exponential(multiplier=1, max=10),
 | 
					    @tenacity.retry(wait=tenacity.wait_exponential(multiplier=1, max=10),
 | 
				
			||||||
@@ -400,7 +401,8 @@ def configure_secrets_backend():
 | 
				
			|||||||
        return
 | 
					        return
 | 
				
			||||||
    client.auth_approle(charm_role_id)
 | 
					    client.auth_approle(charm_role_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    secrets = endpoint_from_flag('endpoint.secrets.new-request')
 | 
					    secrets = (endpoint_from_flag('endpoint.secrets.new-request') or
 | 
				
			||||||
 | 
					               endpoint_from_flag('secrets.connected'))
 | 
				
			||||||
    requests = secrets.requests()
 | 
					    requests = secrets.requests()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Configure KV secret backends
 | 
					    # Configure KV secret backends
 | 
				
			||||||
@@ -411,6 +413,8 @@ def configure_secrets_backend():
 | 
				
			|||||||
            continue
 | 
					            continue
 | 
				
			||||||
        vault.configure_secret_backend(client, name=backend)
 | 
					        vault.configure_secret_backend(client, name=backend)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    refresh_secrets = is_flag_set('secrets.refresh')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Configure AppRoles for application unit access
 | 
					    # Configure AppRoles for application unit access
 | 
				
			||||||
    for request in requests:
 | 
					    for request in requests:
 | 
				
			||||||
        # NOTE: backends must start with charm-
 | 
					        # NOTE: backends must start with charm-
 | 
				
			||||||
@@ -437,16 +441,27 @@ def configure_secrets_backend():
 | 
				
			|||||||
                                       hostname=hostname)
 | 
					                                       hostname=hostname)
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        cidr = '{}/32'.format(access_address)
 | 
				
			||||||
 | 
					        new_role = (approle_name not in client.list_roles())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        approle_id = vault.configure_approle(
 | 
					        approle_id = vault.configure_approle(
 | 
				
			||||||
            client,
 | 
					            client,
 | 
				
			||||||
            name=approle_name,
 | 
					            name=approle_name,
 | 
				
			||||||
            cidr='{}/32'.format(access_address),
 | 
					            cidr=cidr,
 | 
				
			||||||
            policies=[policy_name])
 | 
					            policies=[policy_name])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if new_role or refresh_secrets:
 | 
				
			||||||
 | 
					            wrapped_secret = vault.generate_role_secret_id(
 | 
				
			||||||
 | 
					                client,
 | 
				
			||||||
 | 
					                name=approle_name,
 | 
				
			||||||
 | 
					                cidr=cidr
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
            secrets.set_role_id(unit=unit,
 | 
					            secrets.set_role_id(unit=unit,
 | 
				
			||||||
                            role_id=approle_id)
 | 
					                                role_id=approle_id,
 | 
				
			||||||
 | 
					                                token=wrapped_secret)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    clear_flag('endpoint.secrets.new-request')
 | 
					    clear_flag('endpoint.secrets.new-request')
 | 
				
			||||||
 | 
					    clear_flag('secrets.refresh')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@when('secrets.connected')
 | 
					@when('secrets.connected')
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -331,6 +331,20 @@ class TestLibCharmVault(unit_tests.test_utils.CharmTestCase):
 | 
				
			|||||||
        vault.configure_secret_backend(hvac_client, 'secrets')
 | 
					        vault.configure_secret_backend(hvac_client, 'secrets')
 | 
				
			||||||
        hvac_client.enable_secret_backend.assert_not_called()
 | 
					        hvac_client.enable_secret_backend.assert_not_called()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_generate_role_secret_id(self):
 | 
				
			||||||
 | 
					        hvac_client = mock.MagicMock()
 | 
				
			||||||
 | 
					        hvac_client.write.return_value = {'wrap_info': {'token': 'foo'}}
 | 
				
			||||||
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            vault.generate_role_secret_id(hvac_client,
 | 
				
			||||||
 | 
					                                          'testrole',
 | 
				
			||||||
 | 
					                                          '10.5.10.10/32'),
 | 
				
			||||||
 | 
					            'foo'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        hvac_client.write.assert_called_with(
 | 
				
			||||||
 | 
					            'auth/approle/role/testrole/secret-id',
 | 
				
			||||||
 | 
					            wrap_ttl='1h', cidr_list='10.5.10.10/32'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_configure_policy(self):
 | 
					    def test_configure_policy(self):
 | 
				
			||||||
        hvac_client = mock.MagicMock()
 | 
					        hvac_client = mock.MagicMock()
 | 
				
			||||||
        vault.configure_policy(hvac_client, 'test-policy', 'test-hcl')
 | 
					        vault.configure_policy(hvac_client, 'test-policy', 'test-hcl')
 | 
				
			||||||
@@ -354,7 +368,7 @@ class TestLibCharmVault(unit_tests.test_utils.CharmTestCase):
 | 
				
			|||||||
            token_ttl='60s',
 | 
					            token_ttl='60s',
 | 
				
			||||||
            token_max_ttl='60s',
 | 
					            token_max_ttl='60s',
 | 
				
			||||||
            policies=['test-policy'],
 | 
					            policies=['test-policy'],
 | 
				
			||||||
            bind_secret_id='false',
 | 
					            bind_secret_id='true',
 | 
				
			||||||
            bound_cidr_list='10.5.0.20/32'
 | 
					            bound_cidr_list='10.5.0.20/32'
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        hvac_client.get_role_id.assert_called_with('test-role')
 | 
					        hvac_client.get_role_id.assert_called_with('test-role')
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -508,6 +508,8 @@ class TestHandlers(unit_tests.test_utils.CharmTestCase):
 | 
				
			|||||||
        _vault.configure_approle.side_effect = ['role_a', 'role_b']
 | 
					        _vault.configure_approle.side_effect = ['role_a', 'role_b']
 | 
				
			||||||
        self.is_flag_set.return_value = False
 | 
					        self.is_flag_set.return_value = False
 | 
				
			||||||
        _vault.get_api_url.return_value = "http://vault:8200"
 | 
					        _vault.get_api_url.return_value = "http://vault:8200"
 | 
				
			||||||
 | 
					        hvac_client.list_roles.return_value = []
 | 
				
			||||||
 | 
					        _vault.generate_role_secret_id.return_value = 'mysecret'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        handlers.configure_secrets_backend()
 | 
					        handlers.configure_secrets_backend()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -533,12 +535,17 @@ class TestHandlers(unit_tests.test_utils.CharmTestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        secrets_interface.set_role_id.assert_has_calls([
 | 
					        secrets_interface.set_role_id.assert_has_calls([
 | 
				
			||||||
            mock.call(unit=mock.ANY,
 | 
					            mock.call(unit=mock.ANY,
 | 
				
			||||||
                      role_id='role_a'),
 | 
					                      role_id='role_a',
 | 
				
			||||||
 | 
					                      token='mysecret'),
 | 
				
			||||||
            mock.call(unit=mock.ANY,
 | 
					            mock.call(unit=mock.ANY,
 | 
				
			||||||
                      role_id='role_b'),
 | 
					                      role_id='role_b',
 | 
				
			||||||
 | 
					                      token='mysecret'),
 | 
				
			||||||
        ])
 | 
					        ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.clear_flag.assert_called_once_with('endpoint.secrets.new-request')
 | 
					        self.clear_flag.assert_has_calls([
 | 
				
			||||||
 | 
					            mock.call('endpoint.secrets.new-request'),
 | 
				
			||||||
 | 
					            mock.call('secrets.refresh'),
 | 
				
			||||||
 | 
					        ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @mock.patch.object(handlers, 'vault')
 | 
					    @mock.patch.object(handlers, 'vault')
 | 
				
			||||||
    def send_vault_url_and_ca(self, _vault):
 | 
					    def send_vault_url_and_ca(self, _vault):
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user