diff --git a/roles/koji-hub/defaults/main.yml b/roles/koji-hub/defaults/main.yml
new file mode 100644
index 0000000..373abab
--- /dev/null
+++ b/roles/koji-hub/defaults/main.yml
@@ -0,0 +1,10 @@
+koji_uid: 998
+koji_gid: 996
+koji_home: /var/lib/koji
+koji_db_name: koji
+koji_db_user: "{{ koji_db_name }}"
+koji_hub_disable_notifications: False
+kojihub_host: '{{ ansible_fqdn }}'
+kojiweb_hostname: "{{ kojihub_host }}"
+kojiweb_url: https://{{ kojiweb_hostname }}/koji
+koji_admin_user: kojiadmin
diff --git a/roles/koji-hub/handlers/main.yml b/roles/koji-hub/handlers/main.yml
new file mode 100644
index 0000000..180fb77
--- /dev/null
+++ b/roles/koji-hub/handlers/main.yml
@@ -0,0 +1,14 @@
+- name: load koji db schema
+ become: true
+ become_user: koji
+ shell:
+ psql {{ koji_db_name }} -f /usr/share/doc/koji-*/docs/schema.sql;
+- name: create koji admin user
+ become: true
+ become_user: koji
+ command: >-
+ psql {{ koji_db_name }} -c "WITH newuser AS (
+ INSERT INTO users (name, status, usertype)
+ VALUES ('{{ koji_admin_user }}', 0, 0) RETURNING id)
+ INSERT INTO user_perms (user_id, perm_id, creator_id)
+ SELECT id, 1, id FROM newuser;"
diff --git a/roles/koji-hub/tasks/main.yml b/roles/koji-hub/tasks/main.yml
new file mode 100644
index 0000000..df65c33
--- /dev/null
+++ b/roles/koji-hub/tasks/main.yml
@@ -0,0 +1,82 @@
+- name: load distribution-specific values
+ include_vars: '{{ item }}'
+ with_first_found:
+ - '{{ ansible_distribution }}.yml'
+ - defaults.yml
+ tags:
+ - always
+
+- name: ensure packages are installed
+ package:
+ name={{ koji_hub_packages|join(',') }}
+ state=present
+ tags:
+ - install
+ notify: restart httpd
+- meta: flush_handlers
+
+- name: ensure koji group exists
+ group:
+ name=koji
+ gid={{ koji_gid }}
+ state=present
+- name: ensure koji user exists
+ user:
+ name=koji
+ home={{ koji_home }}
+ createhome=no
+ group=koji
+ uid={{ koji_uid }}
+ state=present
+
+- name: ensure koji db user exists
+ become: true
+ become_user: postgres
+ postgresql_user:
+ name={{ koji_db_user }}
+ state=present
+- name: ensure koji db exists
+ become: true
+ become_user: postgres
+ postgresql_db:
+ name={{ koji_db_name }}
+ owner={{ koji_db_user }}
+ state=present
+ notify:
+ - load koji db schema
+ - create koji admin user
+
+- name: ensure koji filesystem layout is set up
+ file:
+ path={{ koji_home }}/{{ item }}
+ owner=koji
+ group=koji
+ setype=public_content_rw_t
+ state=directory
+ with_items:
+ - packages
+ - repos
+ - repos-dist
+ - scratch
+ - work
+
+- name: ensure koji hub is configured
+ template:
+ src=hub.conf.j2
+ dest=/etc/koji-hub/hub.conf
+ mode=0644
+ notify: reload httpd
+
+- name: ensure apache is configured to serve koji hub
+ template:
+ src=kojihub.httpd.conf.j2
+ dest=/etc/httpd/conf.d/kojihub.conf
+ notify: reload httpd
+- name: ensure selinux is configured for koji hub
+ seboolean:
+ name={{ item }}
+ persistent=yes
+ state=yes
+ with_items:
+ - httpd_can_network_connect_db
+ - httpd_anon_write
diff --git a/roles/koji-hub/templates/hub.conf.j2 b/roles/koji-hub/templates/hub.conf.j2
new file mode 100644
index 0000000..7299f4b
--- /dev/null
+++ b/roles/koji-hub/templates/hub.conf.j2
@@ -0,0 +1,117 @@
+[hub]
+
+## ConfigParser style config file, similar to ini files
+## http://docs.python.org/library/configparser.html
+##
+## Note that multiline values can be set by indenting subsequent lines
+## (which means you should not indent regular lines)
+
+## Basic options ##
+DBName = {{ koji_db_name }}
+DBUser = {{ koji_db_user }}
+{% if ansible_distribution_major_version|int > 6 %}
+DBHost = /run/postgresql
+{% else %}
+#DBHost = db.example.com
+{% endif %}
+#DBPass = example_password
+KojiDir = {{ koji_home }}
+
+
+## Kerberos authentication options ##
+
+# AuthPrincipal = host/kojihub@EXAMPLE.COM
+# AuthKeytab = /etc/koji.keytab
+# ProxyPrincipals = koji/kojiweb@EXAMPLE.COM
+## format string for host principals (%s = hostname)
+# HostPrincipalFormat = compile/%s@EXAMPLE.COM
+
+## end Kerberos auth configuration
+
+
+
+## SSL client certificate auth configuration ##
+#note: ssl auth may also require editing the httpd config (conf.d/kojihub.conf)
+
+## the client username is the common name of the subject of their client certificate
+DNUsernameComponent = CN
+## separate multiple DNs with |
+{% if koji_hub_proxy_dns is defined %}
+ProxyDNs = {{ koji_hub_proxy_dns|join('|\n ') }}
+{% else %}
+# ProxyDNs = /C=US/ST=Massachusetts/O=Example Org/OU=Example User/CN=example/emailAddress=example@example.com
+{% endif %}
+
+## end SSL client certificate auth configuration
+
+
+
+## Other options ##
+LoginCreatesUser = On
+KojiWebURL = {{ kojiweb_url }}
+# The domain name that will be appended to Koji usernames
+# when creating email notifications
+{% if koji_email_domain is defined %}
+EmailDomain = {{ koji_email_domain }}
+{% else %}
+#EmailDomain = example.com
+{% endif %}
+# whether to send the task owner and package owner email or not on success. this still goes to watchers
+NotifyOnSuccess = True
+## Disables all notifications
+DisableNotifications = {{ koji_hub_disable_notifications }}
+{% if not koji_hub_check_client_ip %}
+# Disable client IP address check, allowing clients to use
+# the same session with multiple source addresses (e.g. from
+# behind a proxy or when the client's address changes.
+CheckHostIP = False
+{% endif %}
+
+## Extended features
+## Support Maven builds
+# EnableMaven = False
+## Support Windows builds
+# EnableWin = False
+
+## Koji hub plugins
+## The path where plugins are found
+# PluginPath = /usr/lib/koji-hub-plugins
+## A space-separated list of plugins to load
+{% if koji_hub_plugins is defined %}
+Plugins = {{ koji_hub_plugins|join(' ') }}
+{% else %}
+# Plugins = echo
+{% endif %}
+
+## If KojiDebug is on, the hub will be /very/ verbose and will report exception
+## details to clients for anticipated errors (i.e. koji's own exceptions --
+## subclasses of koji.GenericError).
+# KojiDebug = On
+
+## Determines how much detail about exceptions is reported to the client (via faults)
+## Meaningful values:
+## normal - a basic traceback (format_exception)
+## extended - an extended traceback (format_exc_plus)
+## anything else - no traceback, just the error message
+## The extended traceback is intended for debugging only and should NOT be
+## used in production, since it may contain sensitive information.
+# KojiTraceback = normal
+
+## These options are intended for planned outages
+# ServerOffline = False
+# OfflineMessage = temporary outage
+# LockOut = False
+## If ServerOffline is True, the server will always report a ServerOffline fault (with
+## OfflineMessage as the fault string).
+## If LockOut is True, the server will report a ServerOffline fault for all non-admin
+## requests.
+{% if koji_hub_policy is defined %}
+
+[policy]
+{% for policy in koji_hub_policy %}
+{{ policy.name }} =
+{% for rule in policy.rules %}
+ {{ rule }}
+{% endfor %}
+{% endfor %}
+{% endif %}
diff --git a/roles/koji-hub/templates/kojihub.httpd.conf.j2 b/roles/koji-hub/templates/kojihub.httpd.conf.j2
new file mode 100644
index 0000000..0c89f07
--- /dev/null
+++ b/roles/koji-hub/templates/kojihub.httpd.conf.j2
@@ -0,0 +1,55 @@
+#
+# koji-hub is an xmlrpc interface to the Koji database
+#
+
+WSGIDaemonProcess koji user=koji group=koji display-name=%{GROUP} processes=4 threads=1
+WSGIScriptAlias /kojihub /usr/share/koji-hub/kojixmlrpc.py process-group=koji
+
+
+ Options ExecCGI
+
+ Order allow,deny
+ Allow from all
+
+ = 2.4>
+ Require all granted
+
+
+
+# Also serve /mnt/koji
+Alias /kojifiles "{{ koji_home }}"
+
+
+ Options Indexes FollowSymLinks
+ AllowOverride None
+
+ Order allow,deny
+ Allow from all
+
+ = 2.4>
+ Require all granted
+
+
+
+ Deny from all
+
+ = 2.4>
+ Require all denied
+
+
+
+
+# uncomment this to enable authentication via SSL client certificates
+
+ SSLVerifyClient require
+ SSLVerifyDepth 10
+ SSLOptions +StdEnvVars
+
+
+# If you need to support koji < 1.4.0 clients using SSL authentication, then use the following instead:
+#
+# SSLOptions +StdEnvVars
+#
+# In this case, you will need to enable these options globally (in ssl.conf):
+# SSLVerifyClient require
+# SSLVerifyDepth 10
diff --git a/roles/koji-hub/vars/CentOS.yml b/roles/koji-hub/vars/CentOS.yml
new file mode 100644
index 0000000..545b98d
--- /dev/null
+++ b/roles/koji-hub/vars/CentOS.yml
@@ -0,0 +1,4 @@
+koji_hub_packages:
+- koji-hub
+- libsemanage-python
+- python-psycopg2
diff --git a/roles/koji-hub/vars/defaults.yml b/roles/koji-hub/vars/defaults.yml
new file mode 100644
index 0000000..39411ba
--- /dev/null
+++ b/roles/koji-hub/vars/defaults.yml
@@ -0,0 +1,3 @@
+koji_hub_packages:
+- koji-hub
+- python3-psycopg2