hvac: Missing client token with approle

Hi,

Vault 0.11.3 and hvac 0.6.4.

Probably not an issue, but this “used to work before” (like Vault 0.10 and hvac 5.x).

Reproducing the following flow: approle

Piece of the code failing:

def get_vault_token(vault_rolename):
    # get role id from vault-tower
    role_id_json = requests_wrap.get_data("{}{}/{}".format(vault_token_tower, "/roleid", vault_rolename))
    role_id = role_id_json['role_id']
    print("roleid: {}".format(role_id))

    # get secret wrap from vault-tower
    wrapped_token_json = requests_wrap.post_data("{}{}/{}".format(vault_token_tower, "/wraptoken", vault_rolename))
    wrapped_token = wrapped_token_json['wrap_token']
    print("wrapped token: {}".format(wrapped_token))

    # unwrap token to get secretid
   # Failing here
    unwrapped_secret_id = vc.unwrap(wrapped_token)
    print(unwrapped_secret_id['data']['secret_id'])
    print("Secret id: {}".format(unwrapped_secret_id))

with the following – see https://gist.github.com/madchap/f7efd023c9c6f6058c4b8192aa97323c

The policy associated with the role is the following:

# Login with AppRole
path "auth/approle/login" {
  capabilities = [ "create", "read" ]
}

path "auth/approle/role/torrent9scraper" {
  capabilities = [ "create", "update", "read" ]
}

Basically, any action with object vc fails with that error (i.e- vc.set_role_id(), or vc.create_token()) The vc Client only has the url param passed to it, no token obviously. I am using a simulated “trusted entity” to map to the schema.

With my “admin” profile from the CLI, I am able to unwrap with no problem.

I am a bit at a loss here. I definitely used to be able to unwrap a token this way before.

Thanks for shedding some light. Cheers, fred

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 2
  • Comments: 16 (4 by maintainers)

Most upvoted comments

I think providing an API similar to how the Vault CLI works would probably be the most useful for people. Perhaps following how the Requests library provides responses as an object with response.code for the status code, response.json for the json return, and response.text to dump it in a slightly nicer format for direct printing. Maybe hvac could wrap responses as a Python OrderedDict by default (simple_salesforce does this against the Salesforce API) and then offer an option to just get the raw JSON as mentioned above.

Thanks for the input @reiven! FWIW, along those lines, the follow code snippet is what I’ve personally ended up with in a Django app hosted via Heroku that receives a wrapped secret_id and then instantiates a hvac client using those credentials (this may not be directly related to the most recent comments on this issue, but I figured it would be worth chiming in with my own usage in case its helpful for any other folks while we pin down what problem(s) this issue should be solving for):

import logging

import hvac
from django.conf import settings
from django.core.cache import cache
from hvac.exceptions import VaultError

from apps.vault_client.models import SecretId

logger = logging.getLogger(__name__)

VAULT_TOKEN_CACHE_KEY = 'vault_token'


def verify_then_unwrap_secret_id(response_wrapping_token):
    """Validate a response wrapping token to verify its integrity and then returns its containing secret_id.
    :param response_wrapping_token: str, Vault token that has "wrapped" some underlying data.
    :return: dict, Once the wrapped token has been verified and unwrapped, this method returns a "data" dictionary
    containing a secret_id_accessor and secret_id.
    """
    vault_client = hvac.Client(
        url=settings.VAULT_URL,
    )

    # First lookup the token to ensure it hasn't already been unwrapped (which *could* be bad).
    lookup_response = lookup_wrapped_token(response_wrapping_token=response_wrapping_token)

    # Then verify the creation path string matches expectations.
    # That may catch if some other entity attempted to rewrapped the token in transit.
    if not lookup_response['data']['creation_path'].startswith(settings.VAULT_WRAP_CREATION_PATH_PREFIX):
        error_msg = 'Token creation path "{}" does not match expected path prefix "{}". Unable to continue.'.format(
            lookup_response['data']['creation_path'],
            settings.VAULT_WRAP_CREATION_PATH_PREFIX,
        )
        raise AssertionError(error_msg)

    # Now unwrap the token and verify we received a secret_id.
    unwrapped_response = vault_client.unwrap(token=response_wrapping_token)
    assert 'secret_id' in unwrapped_response.get('data'), '"secret_id" not found in unwrapped_response data.'

    # Attempt to authenticate using the new secret_id. If successful, return the secret_id for later token generation.
    auth_response = vault_client.auth_approle(
        role_id=settings.VAULT_ROLE_ID,
        secret_id=unwrapped_response['data']['secret_id'],
        mount_point=settings.VAULT_APPROLE_MOUNT_POINT,
        use_token=False,
    )
    assert 'client_token' in auth_response.get('auth'), 'Unable to retrieve a "client_token" with provided secret ID.'

    return unwrapped_response.get('data')


def lookup_wrapped_token(response_wrapping_token):
    """Lookup a provided response_wrapping_token and return associated metadata (provided the token has not
    already been unwrapped).
    TODO: replace this with a explicit method in hvac upstream.
    :param response_wrapping_token: str, Vault token that has "wrapped" some underlying data.
    :return: dict, Returns wrapping token properties.
    """
    vault_client = hvac.Client(
        url=settings.VAULT_URL,
    )
    vault_client.token = response_wrapping_token
    token_lookup = vault_client.write(
        path='sys/wrapping/lookup',
    )
    logger.debug('lookup_wrapped_token response: {}'.format(token_lookup))
    return token_lookup


def retrieve_client_token():
    """Attempt to retrieve an existing token from the Django cache if available. 

    If a token is not found in the cache, a new token is generated using the AppRole backend.
    """
    cached_token = cache.get(VAULT_TOKEN_CACHE_KEY)
    if cached_token is None or cached_token.get('client_token') is None:
        vault_client = hvac.Client(
            url=settings.VAULT_URL,
        )
        logger.info('No Vault token found in cache (under key "{}")'.format(VAULT_TOKEN_CACHE_KEY))
        latest_secret_id = SecretId.objects.latest('created')
        auth_response = vault_client.auth_approle(
            role_id=settings.VAULT_ROLE_ID,
            secret_id=latest_secret_id.secret_id,
            mount_point=settings.VAULT_APPROLE_MOUNT_POINT,
            use_token=True,
        )
        if vault_client.is_authenticated():
            cache.set(
                key=VAULT_TOKEN_CACHE_KEY,
                value=auth_response['auth'],
                timeout=auth_response['auth']['lease_duration']
            )
        else:
            error_msg = 'unable to retrieve a new Vault client token'
            logger.error(error_msg)
            raise VaultError(error_msg)
        return auth_response['auth']['client_token']
    else:
        return cached_token['client_token']


def retrieve_authenticated_client():
    """Retrieve a current Vault token and ensure that token is able to authenticate with the
    Vault service.
    :return: hvac.Client with valid token
    """
    vault_client = hvac.Client(
        url=settings.VAULT_URL,
        token=retrieve_client_token(),
    )
    if not vault_client.is_authenticated():
        error_msg = 'unable to retrieve an authenticated Vault client'
        logger.error(error_msg)
        raise VaultError(error_msg)

    return vault_client

[I haven’t actually exercised this code in some time, so I’ll try to loop back on that front and verify if its still relevant for the issue at hand at some point…]

I may have gotten slightly farther by putting token='' into the initial constructor, now I’m fighting the AppRole policies which may not have gotten created properly. I’ll see if I can set up my own dummy instance of Vault with AppRoles/Namespaces and see how hvac handles the mix. I noticed looking at the code that “use_token=True” was scattered through a lot of methods, it seems like it might be useful to create an exception for “InvalidMissingVaultToken” and simply have any methods that need an actual token to raise the exception if it wasn’t defined via auth_approle or passed in through the environment.