some modifications - VaultPass GPG-encrypted creds are almost working.
This commit is contained in:
parent
feb032b84f
commit
2545138ae1
@ -175,7 +175,8 @@ To determine the behaviour of how this behaves, please refer to the below table.
|
|||||||
<!-- SNIP -->
|
<!-- SNIP -->
|
||||||
<auth>
|
<auth>
|
||||||
<!-- "Automagic" (#1).
|
<!-- "Automagic" (#1).
|
||||||
First $VAULT_TOKEN environment variable is checked, then ~/.vault-token is checked. -->
|
First $VAULT_TOKEN environment variable is checked,
|
||||||
|
then ~/.vault-token is checked. -->
|
||||||
<token/>
|
<token/>
|
||||||
|
|
||||||
<!-- Source is considered the only place to fetch token from (#2). -->
|
<!-- Source is considered the only place to fetch token from (#2). -->
|
||||||
@ -232,8 +233,8 @@ To get around needing to store plaintext credentials on-disk in any form, VaultP
|
|||||||
elements. These elements are of the same composition (described <<gpg_elements, below>>) and allow you to use GPG to
|
elements. These elements are of the same composition (described <<gpg_elements, below>>) and allow you to use GPG to
|
||||||
encrypt that sensitive information.
|
encrypt that sensitive information.
|
||||||
|
|
||||||
Note that while this does increase security, it breaks compatibility with other XML parsers - they won't be able to
|
While this does increase security, it breaks compatibility with other XML parsers - they won't be able to decrypt and
|
||||||
decrypt and parse the encrypted snippet unless explicitly coded to do so.
|
parse the encrypted snippet unless explicitly coded to do so.
|
||||||
|
|
||||||
==== `*Gpg` elements
|
==== `*Gpg` elements
|
||||||
`*Gpg` elements (`authGpg`, `unsealGpg`) have the same structure:
|
`*Gpg` elements (`authGpg`, `unsealGpg`) have the same structure:
|
||||||
@ -241,23 +242,16 @@ decrypt and parse the encrypted snippet unless explicitly coded to do so.
|
|||||||
. `unsealGpg`/`authGpg`, the container element.
|
. `unsealGpg`/`authGpg`, the container element.
|
||||||
.. The path to the encrypted file as the contained text.
|
.. The path to the encrypted file as the contained text.
|
||||||
|
|
||||||
It has some optional attributes as well:
|
It has one optional attribute, `gpgHome` footnote:optelem[] -- the GPG home directory to use. If not specified,
|
||||||
|
VaultPass will first check the **`GNUPGHOME`** environment variable. If that isn't defined, we'll default to
|
||||||
.`*Gpg` element attributes
|
`~/.gnupg/` (or whatever the compiled-in default is).
|
||||||
|===
|
|
||||||
|Attribute |Content
|
|
||||||
|
|
||||||
|`keyFPR` footnote:optelem[] | The GPG key to use to decrypt the file. It accepts multiple key ID formats, but it's *highly* recommended to
|
|
||||||
use the full 40-character (without spaces) key fingerprint. If not specified, VaultPass will use the first private key
|
|
||||||
it finds in the keyring.
|
|
||||||
|`gpgHome` footnote:optelem[] | The GPG home directory. If not specified, VaultPass will first check the **`GNUPGHOME`** environment
|
|
||||||
variable. If that's empty, we'll default to `~/.gnupg/`.
|
|
||||||
|===
|
|
||||||
|
|
||||||
The contents of the encrypted file should match the **unencrypted** XML content it's replacing.
|
The contents of the encrypted file should match the **unencrypted** XML content it's replacing.
|
||||||
|
|
||||||
CAUTION: Note that if you use namespaces in your `vaultpass.xml` config file, you **MUST** use matching declarations in
|
CAUTION: Note that if you use namespaces in your `vaultpass.xml` config file, you **MUST** use matching declarations in
|
||||||
your encrypted file.
|
your encrypted file. You **MAY** exclude the `xsi:schemaLocation` specification, however, if it's the same as your
|
||||||
|
`vaultpass.xml`. It is **highly** recommended that you use the same xsi:shemaLocation, however (or leave it out
|
||||||
|
entirely).
|
||||||
|
|
||||||
Let's look at an example of GPG-encrypted elements.
|
Let's look at an example of GPG-encrypted elements.
|
||||||
|
|
||||||
@ -273,11 +267,9 @@ Let's look at an example of GPG-encrypted elements.
|
|||||||
|
|
||||||
<server>
|
<server>
|
||||||
<uri>http://localhost:8000/</uri>
|
<uri>http://localhost:8000/</uri>
|
||||||
<unsealGpg keyFPR="D34DB33FD34DB33FD34DB33FD34DB33FD34DB33F"
|
<unsealGpg gpgHome="~/.gnupg">~/.private/vaultpass/unseal.asc</unsealGpg>
|
||||||
gpgHome="~/.gnupg">~/.private/vaultpass/unseal.asc</unsealGpg>
|
|
||||||
</server>
|
</server>
|
||||||
<authGpg keyFPR="D34DB33FD34DB33FD34DB33FD34DB33FD34DB33F"
|
<authGpg gpgHome="~/.gnupg">~/.private/vaultpass/auth.gpg</unsealGpg>
|
||||||
gpgHome="~/.gnupg">~/.private/vaultpass/auth.gpg</unsealGpg>
|
|
||||||
</vaultpass>
|
</vaultpass>
|
||||||
----
|
----
|
||||||
|
|
||||||
|
7
testxml.py
Executable file
7
testxml.py
Executable file
@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import vaultpass
|
||||||
|
|
||||||
|
cfg = vaultpass.config.LocalFile('/tmp/vaultpass.xml')
|
||||||
|
cfg.main()
|
||||||
|
|
@ -16,7 +16,6 @@ class PassMan(object):
|
|||||||
|
|
||||||
def __init__(self, cfg = '~/.config/vaultpass.xml'):
|
def __init__(self, cfg = '~/.config/vaultpass.xml'):
|
||||||
self.cfg = config.getConfig(cfg)
|
self.cfg = config.getConfig(cfg)
|
||||||
self.cfg.main()
|
|
||||||
self._getURI()
|
self._getURI()
|
||||||
self.getClient()
|
self.getClient()
|
||||||
|
|
||||||
@ -29,6 +28,8 @@ class PassMan(object):
|
|||||||
def getClient(self):
|
def getClient(self):
|
||||||
# This may need to be re-tooled in the future.
|
# This may need to be re-tooled in the future.
|
||||||
auth_xml = self.cfg.xml.find('auth')
|
auth_xml = self.cfg.xml.find('auth')
|
||||||
|
if auth_xml is None:
|
||||||
|
raise RuntimeError('Could not find authentication')
|
||||||
authmethod_xml = auth_xml.getchildren()[0]
|
authmethod_xml = auth_xml.getchildren()[0]
|
||||||
for a in dir(auth):
|
for a in dir(auth):
|
||||||
if a.startswith('_'):
|
if a.startswith('_'):
|
||||||
|
@ -128,6 +128,7 @@ class Token(_AuthBase):
|
|||||||
self.token = self._getEnv(e)
|
self.token = self._getEnv(e)
|
||||||
else:
|
else:
|
||||||
self.token = self._getFile(a)
|
self.token = self._getFile(a)
|
||||||
|
self.client = hvac.Client(url = self.uri)
|
||||||
self.client.token = self.token
|
self.client.token = self.token
|
||||||
self.authCheck()
|
self.authCheck()
|
||||||
return(None)
|
return(None)
|
||||||
|
@ -3,6 +3,7 @@ import os
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
##
|
##
|
||||||
|
from . import gpg_handler
|
||||||
import requests
|
import requests
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
@ -12,6 +13,8 @@ _logger = logging.getLogger()
|
|||||||
|
|
||||||
|
|
||||||
class Config(object):
|
class Config(object):
|
||||||
|
gpg = None
|
||||||
|
gpg_elems = ('authGpg', 'unsealGpg')
|
||||||
xsd_path = None
|
xsd_path = None
|
||||||
tree = None
|
tree = None
|
||||||
namespaced_tree = None
|
namespaced_tree = None
|
||||||
@ -31,6 +34,31 @@ class Config(object):
|
|||||||
self.populateDefaults()
|
self.populateDefaults()
|
||||||
if validate:
|
if validate:
|
||||||
self.validate()
|
self.validate()
|
||||||
|
g = self.parseGpg()
|
||||||
|
if g:
|
||||||
|
# And do it again.
|
||||||
|
if populate_defaults:
|
||||||
|
self.populateDefaults()
|
||||||
|
if validate:
|
||||||
|
self.validate()
|
||||||
|
return(None)
|
||||||
|
|
||||||
|
def decryptGpg(self, gpg_xml):
|
||||||
|
home = gpg_xml.attrib.get('gpgHome')
|
||||||
|
tag = gpg_xml.tag
|
||||||
|
ns_xml = self.xml.find(tag)
|
||||||
|
xml = self.stripNS(obj = ns_xml).tag
|
||||||
|
fpath = gpg_xml.text
|
||||||
|
if not self.gpg:
|
||||||
|
self.gpg = gpg_handler.GPG(home = home)
|
||||||
|
else:
|
||||||
|
self.gpg.gpg.home = home
|
||||||
|
self.gpg.initHome()
|
||||||
|
ns_dcrpt_xml = etree.fromstring(self.gpg.decrypt(fpath))
|
||||||
|
dcrpt_xml = self.stripNS(obj = ns_dcrpt_xml)
|
||||||
|
ns_xml.getparent().replace(ns_xml, ns_dcrpt_xml)
|
||||||
|
xml.getparent().replace(xml, dcrpt_xml)
|
||||||
|
self.parse()
|
||||||
return(None)
|
return(None)
|
||||||
|
|
||||||
def fetch(self): # Just a fail-safe; this is overridden by specific subclasses.
|
def fetch(self): # Just a fail-safe; this is overridden by specific subclasses.
|
||||||
@ -87,11 +115,8 @@ class Config(object):
|
|||||||
_logger.info('Rendered XSD.')
|
_logger.info('Rendered XSD.')
|
||||||
return(None)
|
return(None)
|
||||||
|
|
||||||
def parseRaw(self, parser = None):
|
def parse(self):
|
||||||
self.xml = etree.fromstring(self.raw, parser = parser)
|
# This can used to "re-parse" the self.xml and self.namespaced_xml.
|
||||||
_logger.debug('Generated xml.')
|
|
||||||
self.namespaced_xml = etree.fromstring(self.raw, parser = parser)
|
|
||||||
_logger.debug('Generated namespaced xml.')
|
|
||||||
self.tree = self.xml.getroottree()
|
self.tree = self.xml.getroottree()
|
||||||
_logger.debug('Generated tree.')
|
_logger.debug('Generated tree.')
|
||||||
self.namespaced_tree = self.namespaced_xml.getroottree()
|
self.namespaced_tree = self.namespaced_xml.getroottree()
|
||||||
@ -103,6 +128,26 @@ class Config(object):
|
|||||||
self.stripNS()
|
self.stripNS()
|
||||||
return(None)
|
return(None)
|
||||||
|
|
||||||
|
def parseGpg(self):
|
||||||
|
gpg_elem_found = False # Change to True if we find any GPG-encrypted elems
|
||||||
|
search = []
|
||||||
|
for x in self.gpg_elems:
|
||||||
|
search.append("local-name()='{0}'".format(x))
|
||||||
|
search = '[{0}]'.format(' or '.join(search))
|
||||||
|
print(search)
|
||||||
|
gpg_elems = self.namespaced_xml.findall('|'.join(search))
|
||||||
|
for e in gpg_elems:
|
||||||
|
print(e)
|
||||||
|
return(gpg_elem_found)
|
||||||
|
|
||||||
|
def parseRaw(self, parser = None):
|
||||||
|
self.xml = etree.fromstring(self.raw, parser = parser)
|
||||||
|
_logger.debug('Generated xml.')
|
||||||
|
self.namespaced_xml = etree.fromstring(self.raw, parser = parser)
|
||||||
|
_logger.debug('Generated namespaced xml.')
|
||||||
|
self.parse()
|
||||||
|
return(None)
|
||||||
|
|
||||||
def populateDefaults(self):
|
def populateDefaults(self):
|
||||||
_logger.info('Populating missing values with defaults from XSD.')
|
_logger.info('Populating missing values with defaults from XSD.')
|
||||||
if not self.xsd:
|
if not self.xsd:
|
||||||
|
39
vaultpass/gpg_handler.py
Normal file
39
vaultpass/gpg_handler.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import io
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
##
|
||||||
|
import gpg # https://pypi.org/project/gpg/
|
||||||
|
|
||||||
|
|
||||||
|
_logger = logging.getLogger()
|
||||||
|
|
||||||
|
|
||||||
|
class GPG(object):
|
||||||
|
home = None
|
||||||
|
gpg = None
|
||||||
|
|
||||||
|
def __init__(self, home = None):
|
||||||
|
if home:
|
||||||
|
self.home = home
|
||||||
|
self.initHome()
|
||||||
|
|
||||||
|
def decrypt(self, fpath):
|
||||||
|
fpath = os.path.abspath(os.path.expanduser(fpath))
|
||||||
|
with open(fpath, 'rb') as fh:
|
||||||
|
iobuf = io.BytesIO(fh.read())
|
||||||
|
iobuf.seek(0, 0)
|
||||||
|
rslt = self.gpg.decrypt(iobuf)
|
||||||
|
decrypted = rslt[0]
|
||||||
|
return(decrypted)
|
||||||
|
|
||||||
|
def initHome(self):
|
||||||
|
if not self.home:
|
||||||
|
h = os.environ.get('GNUPGHOME')
|
||||||
|
if h:
|
||||||
|
self.home = h
|
||||||
|
if self.home:
|
||||||
|
self.home = os.path.abspath(os.path.expanduser(self.home))
|
||||||
|
if not os.path.isdir(self.home):
|
||||||
|
raise ValueError('GPG home does not exist')
|
||||||
|
_logger.debug('Set GPG home to explicitly specified value {0}'.format(self.home))
|
||||||
|
return(None)
|
@ -1,16 +0,0 @@
|
|||||||
import os
|
|
||||||
import logging
|
|
||||||
##
|
|
||||||
import gpg
|
|
||||||
|
|
||||||
|
|
||||||
# Special shoutout to Jthan for ruining my life.
|
|
||||||
|
|
||||||
|
|
||||||
_logger = logging.getLogger()
|
|
||||||
|
|
||||||
|
|
||||||
class GPGAuth(object):
|
|
||||||
def __init__(self, gpgauth_xml):
|
|
||||||
pass
|
|
||||||
|
|
Reference in New Issue
Block a user