httpd_configmap_generator 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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