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

External auth support and vault host selection #35

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
36 changes: 36 additions & 0 deletions lib/hiera/backend/sample.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env python
'''
Sample authentication script for Kerberos authentication
of the FreeIPA enrolled host against Vault server.

Arguments:
hostname - Vault server URL (without protocol)
auth_type - host or user

Returns authentication token (usually valid for 30 minutes).
'''

import argparse
import kerberos
import requests


parser = argparse.ArgumentParser()
parser.add_argument('url', help='Vault server URL (without protocol)')
parser.add_argument('type', choices=('host', 'user'))
args = parser.parse_args()

service = 'HTTP@%s' % args.url
mechanism = kerberos.GSS_MECH_OID_SPNEGO
_, ctx = kerberos.authGSSClientInit(service, mech_oid=mechanism)

kerberos.authGSSClientStep(ctx, '')
kerberos_token = kerberos.authGSSClientResponse(ctx)

url = 'https://%s/v1/auth/%ss/login' % (args.url, args.type)
data = {'authorization': 'Negotiate %s' % kerberos_token}
r = requests.post(url, json=data, verify='/etc/ipa/ca.crt')
if r.ok:
print r.json()['auth']['client_token']
else:
raise Exception('Error authenticating: %s' % r.json())
105 changes: 98 additions & 7 deletions lib/hiera/backend/vault_backend.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,48 @@
# Vault backend for Hiera
class Hiera
# Due to the authentication information might be not avaliable
# on the moment when the Puppet is runed for the first time,
# addig the variable that indicates if the actual authentication
# already happened.
# For example if it's the first pupper run ever, the host is not enrolled in
# the domain yet and there is no Kerberos on this stage but it will
# be when someone will actually try to read value from the Vault. At this
# moment the actuall authentication would happen.
# Options for:
# proto - protocol (http/htts)
# port - port where vault server litens
# fqdn_expand - short hostname given, expend it
# auth_type - if "external", use external command for authentication
# cmd - command to run for the authentication. Should just return
# tokn in the stdout. Should accept hostname + extra optional
# arguments.
# args - extra arguments for the command defined in the "cmd"
initialized = false
module RunCmd
module_function

# @param [String] cmd -> command to run
# @return [String] -> Stdout
# @throws [RuntimeError] -> includes the Stderr

def cmd command
require 'open3'
stdout_str, stderr_str, status = Open3.capture3(command)
fail "#{command}: #{stderr_str.chomp}" unless status.success?
stdout_str
end
end

module Backend
class Vault_backend

def initialize()
Hiera.debug("[hiera-vault] backned is loaded")
end
def initialize_vault()
require 'json'
require 'vault'

require 'socket'
@config = Config[:vault]
@config[:mounts] ||= {}
@config[:mounts][:generic] ||= ['secret']
Expand All @@ -26,10 +62,58 @@ def initialize()
end

begin

# Unless [:fqdn_expand] is set to 'false' we use the host name as it
# is. If it is set to 'true' we expand the name. This is used to
# address correct Vault server from the cluster based on the DNS
# information. Might not be needed for all users, so if there is
# no setting given nothing will happen.
fqdn_expand = @config[:fqdn_expand] unless @config[:addr].nil?
if fqdn_expand
short_hostname = @config[:addr] unless @config[:addr].nil?
Hiera.debug("[hiera-vault] Expandin hostname #{short_hostname} to FQDN")
vault_hostname = Socket.gethostbyname(short_hostname).first
else
vault_hostname = @config[:addr] unless @config[:addr].nil?
end
Hiera.debug("[hiera-vault] Vault hostname: #{vault_hostname}")

# We can have "expternal" authentication type:
# anything or absend -> default. Host and token are hardcoded
# into the hiera.yaml
# "external" -> some external program returns access token string
# if no setting given, assume that authentication token is
# hardcoded in the hiera.yaml
auth_type = @config[:auth_type] unless @config[:auth_type].nil?
if auth_type == 'external'
Hiera.debug("[hiera-vault] Using external authentication")
token_cmd = @config[:cmd] unless @config[:cmd].nil?
token_cmd = token_cmd + " " + vault_hostname
if @config[:args]
token_cmd = token_cmd + " " + @config[:args]
end
Hiera.debug("[hiera-vault] Command: #{token_cmd}")
token_result = RunCmd::cmd(token_cmd)
else
Hiera.debug("[hiera-vault] Using hardcoded authentication")
token_result = @config[:token] unless @config[:token].nil?
end
port = @config[:port] unless @config[:port].nil?
@vault = Vault::Client.new
@vault.configure do |config|
config.address = @config[:addr] unless @config[:addr].nil?
config.token = @config[:token] unless @config[:token].nil?

# If we have "proto" in the config then we have new styled config
# in the other case just use the hostname + proto + port as is
# from the config. "Proto" is defining 443 in case of the https
# so it's more importnant then the "port".
proto = @config[:proto] unless @config[:proto].nil?
if proto
config.address = proto+vault_hostname+":"+port.to_s
else
config.address = vault_hostname
end
Hiera.debug("[hiera-vault] Will connect to: #{config.address}")
config.token = token_result
config.ssl_pem_file = @config[:ssl_pem_file] unless @config[:ssl_pem_file].nil?
config.ssl_verify = @config[:ssl_verify] unless @config[:ssl_verify].nil?
config.ssl_ca_cert = @config[:ssl_ca_cert] if config.respond_to? :ssl_ca_cert
Expand All @@ -41,18 +125,23 @@ def initialize()
Hiera.debug("[hiera-vault] Client configured to connect to #{@vault.address}")
rescue Exception => e
@vault = nil
Hiera.warn("[hiera-vault] Skipping backend. Configuration error: #{e}")
Hiera.warn("[hiera-vault] Vault configuration failed. Configuration error: #{e}")
end
end

def lookup(key, scope, order_override, resolution_type)
# Here comes 1st actual attempt to authenticate against vault
# Ensuring we are doing this only once
if !@initialized
initialize_vault()
@initialized = true
end
return nil if @vault.nil?

Hiera.debug("[hiera-vault] Looking up #{key} in vault backend")

answer = nil
found = false

# Only generic mounts supported so far
@config[:mounts][:generic].each do |mount|
path = Backend.parse_string(mount, scope, { 'key' => key })
Expand Down Expand Up @@ -112,8 +201,10 @@ def lookup_generic(key, scope)
#Hiera.debug("[hiera-vault] Data: #{data}:#{data.class}")

return Backend.parse_answer(data, scope)
end
end

end
end
end
end