httpd_configmap_generator 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +14 -0
  5. data/Dockerfile +16 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE +201 -0
  8. data/README-active-directory.md +38 -0
  9. data/README-ipa.md +41 -0
  10. data/README-saml.md +70 -0
  11. data/README.md +386 -0
  12. data/Rakefile +8 -0
  13. data/bin/httpd_configmap_generator +101 -0
  14. data/httpd_configmap_generator.gemspec +34 -0
  15. data/lib/httpd_configmap_generator.rb +29 -0
  16. data/lib/httpd_configmap_generator/active_directory.rb +114 -0
  17. data/lib/httpd_configmap_generator/base.rb +83 -0
  18. data/lib/httpd_configmap_generator/base/command.rb +29 -0
  19. data/lib/httpd_configmap_generator/base/config.rb +13 -0
  20. data/lib/httpd_configmap_generator/base/config_map.rb +183 -0
  21. data/lib/httpd_configmap_generator/base/file.rb +66 -0
  22. data/lib/httpd_configmap_generator/base/kerberos.rb +13 -0
  23. data/lib/httpd_configmap_generator/base/network.rb +37 -0
  24. data/lib/httpd_configmap_generator/base/pam.rb +9 -0
  25. data/lib/httpd_configmap_generator/base/principal.rb +33 -0
  26. data/lib/httpd_configmap_generator/base/sssd.rb +51 -0
  27. data/lib/httpd_configmap_generator/export.rb +31 -0
  28. data/lib/httpd_configmap_generator/ipa.rb +122 -0
  29. data/lib/httpd_configmap_generator/options.rb +13 -0
  30. data/lib/httpd_configmap_generator/saml.rb +104 -0
  31. data/lib/httpd_configmap_generator/update.rb +39 -0
  32. data/lib/httpd_configmap_generator/version.rb +3 -0
  33. data/templates/etc/pam.d/httpd-auth +2 -0
  34. data/templates/httpd-configmap-generator-template.yaml +113 -0
  35. metadata +203 -0
@@ -0,0 +1,66 @@
1
+ require "pathname"
2
+
3
+ module HttpdConfigmapGenerator
4
+ class Base
5
+ def template_directory
6
+ @template_directory ||= begin
7
+ Pathname.new(Bundler.locked_gems.specs.select { |g| g.name == "httpd_configmap_generator" }.first.gem_dir).join("templates")
8
+ end
9
+ end
10
+
11
+ def cp_template(file, src_dir, dest_dir = "/")
12
+ src_path = path_join(src_dir, file)
13
+ dest_path = path_join(dest_dir, file.gsub(".erb", ""))
14
+ if src_path.to_s.include?(".erb")
15
+ File.write(dest_path, ERB.new(File.read(src_path), nil, '-').result(binding))
16
+ else
17
+ FileUtils.cp(src_path, dest_path)
18
+ end
19
+ end
20
+
21
+ def delete_target_file(file_path)
22
+ if File.exist?(file_path)
23
+ if opts[:force]
24
+ info_msg("File #{file_path} exists, forcing a delete")
25
+ File.delete(file_path)
26
+ else
27
+ raise "File #{file_path} already exist"
28
+ end
29
+ end
30
+ end
31
+
32
+ def create_target_directory(file_path)
33
+ dirname = File.dirname(file_path)
34
+ return if File.exist?(dirname)
35
+ debug_msg("Creating directory #{dirname} ...")
36
+ FileUtils.mkdir_p(dirname)
37
+ end
38
+
39
+ def rm_file(file, dir = "/")
40
+ path = path_join(dir, file)
41
+ File.delete(path) if File.exist?(path)
42
+ end
43
+
44
+ def path_join(*args)
45
+ path = Pathname.new(args.shift)
46
+ args.each { |path_seg| path = path.join("./#{path_seg}") }
47
+ path
48
+ end
49
+
50
+ def file_binary?(file)
51
+ data = File.read(file)
52
+ ascii = control = binary = total = 0
53
+ data[0..512].each_byte do |c|
54
+ total += 1
55
+ if c < 32
56
+ control += 1
57
+ elsif c >= 32 && c <= 128
58
+ ascii += 1
59
+ else
60
+ binary += 1
61
+ end
62
+ end
63
+ control.to_f / ascii > 0.1 || binary.to_f / ascii > 0.05
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,13 @@
1
+ module HttpdConfigmapGenerator
2
+ class Base
3
+ def enable_kerberos_dns_lookups
4
+ info_msg("Configuring Kerberos DNS Lookups")
5
+ config_file_backup(KERBEROS_CONFIG_FILE)
6
+ krb5config = File.read(KERBEROS_CONFIG_FILE)
7
+ krb5config[/(\s*)dns_lookup_kdc(\s*)=(\s*)(.*)/, 4] = 'true' if krb5config[/(\s*)dns_lookup_kdc(\s*)=/]
8
+ krb5config[/(\s*)dns_lookup_realm(\s*)=(\s*)(.*)/, 4] = 'true' if krb5config[/(\s*)dns_lookup_realm(\s*)=/]
9
+ debug_msg("- Updating #{KERBEROS_CONFIG_FILE}")
10
+ File.write(KERBEROS_CONFIG_FILE, krb5config)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,37 @@
1
+ module HttpdConfigmapGenerator
2
+ class Base
3
+ HOSTNAME_COMMAND = "/usr/bin/hostname".freeze
4
+
5
+ def realm
6
+ domain.upcase
7
+ end
8
+
9
+ def domain
10
+ domain_from_host(opts[:host])
11
+ end
12
+
13
+ def domain_from_host(host)
14
+ host.gsub(/^([^.]+\.)/, '') if host.present? && host.include?('.')
15
+ end
16
+
17
+ def host_reachable?(host)
18
+ require "net/ping"
19
+ Net::Ping::External.new(host).ping
20
+ end
21
+
22
+ def update_hostname(host)
23
+ command_run!(HOSTNAME_COMMAND, :params => [host]) if command_run(HOSTNAME_COMMAND).output.strip != host
24
+ end
25
+
26
+ def fetch_network_file(source_file, target_file)
27
+ require "net/http"
28
+
29
+ delete_target_file(target_file)
30
+ create_target_directory(target_file)
31
+ info_msg("Downloading #{source_file} ...")
32
+ result = Net::HTTP.get_response(URI(source_file))
33
+ raise "Failed to fetch URL file source #{source_file}" unless result.kind_of?(Net::HTTPSuccess)
34
+ File.write(target_file, result.body)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,9 @@
1
+ module HttpdConfigmapGenerator
2
+ class Base
3
+ def configure_pam
4
+ info_msg("Configuring PAM")
5
+ debug_msg("- Creating #{PAM_CONFIG}")
6
+ cp_template(PAM_CONFIG, template_directory)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,33 @@
1
+ require "awesome_spawn"
2
+
3
+ module HttpdConfigmapGenerator
4
+ class Principal < Base
5
+ attr_accessor :hostname
6
+ attr_accessor :realm # EXAMPLE.COM
7
+ attr_accessor :service # HTTP
8
+
9
+ attr_accessor :name # Kerberos principal name generated
10
+
11
+ def initialize(options = {})
12
+ options.each { |n, v| public_send("#{n}=", v) }
13
+ @realm = @realm.upcase if @realm
14
+ @name ||= "#{service}/#{hostname}@#{realm}"
15
+ @name
16
+ end
17
+
18
+ def register
19
+ request unless exist?
20
+ end
21
+
22
+ private
23
+
24
+ def exist?
25
+ command_run(IPA_COMMAND, :params => ["-e", "skip_version_check=1", "service-find", "--principal", name]).success?
26
+ end
27
+
28
+ def request
29
+ # Using --force because these services tend not to be in dns. This is like VERIFY_NONE.
30
+ command_run!(IPA_COMMAND, :params => ["-e", "skip_version_check=1", "service-add", "--force", name])
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,51 @@
1
+ require 'iniparse'
2
+
3
+ module HttpdConfigmapGenerator
4
+ class Sssd < Base
5
+ attr_accessor :sssd
6
+ attr_accessor :opts
7
+
8
+ def initialize(opts = {})
9
+ @opts = opts
10
+ @sssd = nil
11
+ end
12
+
13
+ def load(file_path)
14
+ @sssd = IniParse.open(file_path)
15
+ end
16
+
17
+ def save(file_path)
18
+ return unless sssd
19
+ config_file_backup(file_path)
20
+ info_msg("Saving SSSD to #{file_path}")
21
+ sssd.save(file_path)
22
+ end
23
+
24
+ def configure_domain(domain)
25
+ domain = section("domain/#{domain}")
26
+ domain["ldap_user_extra_attrs"] = LDAP_ATTRS.keys.join(", ")
27
+ domain["entry_cache_timeout"] = 600
28
+ end
29
+
30
+ def add_service(service)
31
+ services = section("sssd")["services"]
32
+ services = (services.split(",").map(&:strip) | [service]).join(", ")
33
+ sssd.section("sssd")["services"] = services
34
+ sssd.section(service)
35
+ end
36
+
37
+ def configure_ifp
38
+ add_service("ifp")
39
+ ifp = section("ifp")
40
+ ifp["allowed_uids"] = "#{APACHE_USER}, root"
41
+ ifp["user_attributes"] = LDAP_ATTRS.keys.collect { |k| "+#{k}" }.join(", ")
42
+ end
43
+
44
+ def section(key)
45
+ if key =~ /^domain\/.*$/
46
+ key = sssd.entries.collect(&:key).select { |k| k.downcase == key.downcase }.first
47
+ end
48
+ sssd.section(key)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,31 @@
1
+ module HttpdConfigmapGenerator
2
+ class Export < Base
3
+ def required_options
4
+ {
5
+ :input => { :description => "Input config map file",
6
+ :short => "-i" },
7
+ :file => { :description => "Config map file to export",
8
+ :short => "-l" },
9
+ :output => { :description => "The output file being exported",
10
+ :short => "-o" }
11
+ }
12
+ end
13
+
14
+ def export(opts)
15
+ validate_options(opts)
16
+ @opts = opts
17
+ config_map = ConfigMap.new(opts)
18
+ config_map.load(opts[:input])
19
+ config_map.export_file(opts[:file], opts[:output])
20
+ rescue => err
21
+ log_command_error(err)
22
+ raise err
23
+ end
24
+
25
+ private
26
+
27
+ def validate_options(options)
28
+ raise "Input configuration map #{options[:input]} does not exist" unless File.exist?(options[:input])
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,122 @@
1
+ module HttpdConfigmapGenerator
2
+ class Ipa < Base
3
+ IPA_INSTALL_COMMAND = "/usr/sbin/ipa-client-install".freeze
4
+ IPA_GETKEYTAB = "/usr/sbin/ipa-getkeytab".freeze
5
+ AUTH = {
6
+ :type => "external",
7
+ :subtype => "ipa"
8
+ }.freeze
9
+
10
+ def required_options
11
+ super.merge(
12
+ :ipa_server => { :description => "IPA Server Fqdn" },
13
+ :ipa_password => { :description => "IPA Server Password" }
14
+ )
15
+ end
16
+
17
+ def optional_options
18
+ super.merge(
19
+ :ipa_principal => { :description => "IPA Server Principal", :default => "admin" },
20
+ :ipa_domain => { :description => "Domain of IPA Server" },
21
+ :ipa_realm => { :description => "Realm of IPA Server" }
22
+ )
23
+ end
24
+
25
+ def persistent_files
26
+ %w(
27
+ /etc/http.keytab
28
+ /etc/ipa/ca.crt
29
+ /etc/ipa/default.conf
30
+ /etc/ipa/nssdb/cert8.db
31
+ /etc/ipa/nssdb/key3.db
32
+ /etc/ipa/nssdb/pwdfile.txt
33
+ /etc/ipa/nssdb/secmod.db
34
+ /etc/krb5.conf
35
+ /etc/krb5.keytab
36
+ /etc/nsswitch.conf
37
+ /etc/openldap/ldap.conf
38
+ /etc/pam.d/fingerprint-auth-ac
39
+ /etc/pam.d/httpd-auth
40
+ /etc/pam.d/password-auth-ac
41
+ /etc/pam.d/postlogin-ac
42
+ /etc/pam.d/smartcard-auth-ac
43
+ /etc/pam.d/system-auth-ac
44
+ /etc/pki/ca-trust/source/ipa.p11-kit
45
+ /etc/sssd/sssd.conf
46
+ /etc/sysconfig/authconfig
47
+ /etc/sysconfig/network
48
+ )
49
+ end
50
+
51
+ def configure(opts)
52
+ update_hostname(opts[:host])
53
+ command_run!(IPA_INSTALL_COMMAND,
54
+ :params => [
55
+ "-N", :force_join, :fixed_primary, :unattended, {
56
+ :realm= => realm,
57
+ :domain= => domain,
58
+ :server= => opts[:ipa_server],
59
+ :principal= => opts[:ipa_principal],
60
+ :password= => opts[:ipa_password]
61
+ }
62
+ ])
63
+ configure_ipa_http_service
64
+ configure_pam
65
+ configure_sssd
66
+ enable_kerberos_dns_lookups
67
+ config_map = ConfigMap.new(opts)
68
+ config_map.generate(AUTH[:type], realm, persistent_files)
69
+ config_map.save(opts[:output])
70
+ rescue => err
71
+ log_command_error(err)
72
+ raise err
73
+ end
74
+
75
+ def configured?
76
+ File.exist?(SSSD_CONFIG)
77
+ end
78
+
79
+ def unconfigure
80
+ return unless configured?
81
+ command_run(IPA_INSTALL_COMMAND, :params => [:uninstall, :unattended])
82
+ end
83
+
84
+ def realm
85
+ @realm ||= opts[:ipa_realm] if opts[:ipa_realm].present?
86
+ @realm ||= domain
87
+ @realm ||= super
88
+ @realm = @realm.upcase
89
+ end
90
+
91
+ def domain
92
+ @domain ||= opts[:ipa_domain] if opts[:ipa_domain].present?
93
+ @domain ||= domain_from_host(opts[:ipa_server]) if opts[:ipa_server].present?
94
+ @domain ||= super
95
+ @domain
96
+ end
97
+
98
+ private
99
+
100
+ def configure_sssd
101
+ info_msg("Configuring SSSD Service")
102
+ sssd = Sssd.new(opts)
103
+ sssd.load(SSSD_CONFIG)
104
+ sssd.configure_domain(domain)
105
+ sssd.add_service("pam")
106
+ sssd.configure_ifp
107
+ debug_msg("- Creating #{SSSD_CONFIG}")
108
+ sssd.save(SSSD_CONFIG)
109
+ end
110
+
111
+ def configure_ipa_http_service
112
+ info_msg("Configuring IPA HTTP Service")
113
+ command_run!("/usr/bin/kinit", :params => [opts[:ipa_principal]], :stdin_data => opts[:ipa_password])
114
+ service = Principal.new(:hostname => opts[:host], :realm => realm, :service => "HTTP")
115
+ service.register
116
+ debug_msg("- Fetching #{HTTP_KEYTAB}")
117
+ command_run!(IPA_GETKEYTAB, :params => {"-s" => opts[:ipa_server], "-k" => HTTP_KEYTAB, "-p" => service.name})
118
+ FileUtils.chown(APACHE_USER, nil, HTTP_KEYTAB)
119
+ FileUtils.chmod(0o600, HTTP_KEYTAB)
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,13 @@
1
+ module HttpdConfigmapGenerator
2
+ def self.required_options
3
+ {
4
+ :host => { :description => "Application Domain" }
5
+ }
6
+ end
7
+
8
+ def self.optional_options
9
+ {
10
+ :force => { :description => "Force configuration if configured already", :default => false }
11
+ }
12
+ end
13
+ end
@@ -0,0 +1,104 @@
1
+ module HttpdConfigmapGenerator
2
+ class Saml < Base
3
+ MELLON_CREATE_METADATA_COMMAND = "/usr/libexec/mod_auth_mellon/mellon_create_metadata.sh".freeze
4
+ SAML2_CONFIG_DIRECTORY = "/etc/httpd/saml2".freeze
5
+ MIQSP_METADATA_FILE = "#{SAML2_CONFIG_DIRECTORY}/miqsp-metadata.xml".freeze
6
+ IDP_METADATA_FILE = "#{SAML2_CONFIG_DIRECTORY}/idp-metadata.xml".freeze
7
+ AUTH = {
8
+ :type => "saml",
9
+ :subtype => "saml"
10
+ }.freeze
11
+
12
+ def required_options
13
+ super
14
+ end
15
+
16
+ def optional_options
17
+ super.merge(
18
+ :keycloak_add_metadata => { :description => "Download and add the Keycloak metadata file",
19
+ :default => false },
20
+ :keycloak_server => { :description => "Keycloak Server Fqdn or IP" },
21
+ :keycloak_realm => { :description => "Keycloak Realm for this client"}
22
+ )
23
+ end
24
+
25
+ def persistent_files
26
+ file_list = %w(
27
+ /etc/httpd/saml2/miqsp-key.key
28
+ /etc/httpd/saml2/miqsp-cert.cert
29
+ /etc/httpd/saml2/miqsp-metadata.xml
30
+ )
31
+ file_list += [IDP_METADATA_FILE] if opts[:keycloak_add_metadata]
32
+ file_list
33
+ end
34
+
35
+ def configure(opts)
36
+ update_hostname(opts[:host])
37
+ Dir.mkdir(SAML2_CONFIG_DIRECTORY)
38
+ Dir.chdir(SAML2_CONFIG_DIRECTORY) do
39
+ command_run!(MELLON_CREATE_METADATA_COMMAND,
40
+ :params => [
41
+ "https://#{opts[:host]}",
42
+ "https://#{opts[:host]}/saml2"
43
+ ])
44
+ rename_mellon_configfiles
45
+ fetch_idp_metadata
46
+ end
47
+ config_map = ConfigMap.new(opts)
48
+ config_map.generate(AUTH[:type], realm, persistent_files)
49
+ config_map.save(opts[:output])
50
+ rescue => err
51
+ log_command_error(err)
52
+ raise err
53
+ end
54
+
55
+ def configured?
56
+ File.exist?(MIQSP_METADATA_FILE)
57
+ end
58
+
59
+ def unconfigure
60
+ return unless configured?
61
+ FileUtils.rm_rf(SAML2_CONFIG_DIRECTORY) if Dir.exist?(SAML2_CONFIG_DIRECTORY)
62
+ end
63
+
64
+ private
65
+
66
+ def validate_options(options)
67
+ super(options)
68
+ if options[:keycloak_add_metadata]
69
+ if options[:keycloak_server] == "" || options[:keycloak_realm] == ""
70
+ raise "Must specify both keycloak-server and keycloak-realm for fetching the IdP metadata file"
71
+ end
72
+ end
73
+ end
74
+
75
+ def rename_mellon_configfiles
76
+ info_msg("Renaming mellon config files")
77
+ Dir.chdir(SAML2_CONFIG_DIRECTORY) do
78
+ Dir.glob("https_*.*") do |mellon_file|
79
+ miq_saml2_file = nil
80
+ case mellon_file
81
+ when /^https_.*\.key$/
82
+ miq_saml2_file = "miqsp-key.key"
83
+ when /^https_.*\.cert$/
84
+ miq_saml2_file = "miqsp-cert.cert"
85
+ when /^https_.*\.xml$/
86
+ miq_saml2_file = "miqsp-metadata.xml"
87
+ end
88
+ if miq_saml2_file
89
+ debug_msg("- renaming #{mellon_file} to #{miq_saml2_file}")
90
+ File.rename(mellon_file, miq_saml2_file)
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ def fetch_idp_metadata
97
+ if opts[:keycloak_add_metadata]
98
+ source_file = "http://#{opts[:keycloak_server]}:8080"
99
+ source_file += "/auth/realms/#{opts[:keycloak_realm]}/protocol/saml/descriptor"
100
+ fetch_network_file(source_file, IDP_METADATA_FILE)
101
+ end
102
+ end
103
+ end
104
+ end