diff --git a/playbooks/groups/fas.yml b/playbooks/groups/fas.yml index b46ef725f4..cf4cd3615a 100644 --- a/playbooks/groups/fas.yml +++ b/playbooks/groups/fas.yml @@ -41,6 +41,7 @@ - rsyncd - fas_server - sudo + - totpcgi tasks: - include: "{{ tasks }}/yumrepos.yml" diff --git a/roles/totpcgi/files/html/error.html b/roles/totpcgi/files/html/error.html new file mode 100644 index 0000000000..7229ce49d8 --- /dev/null +++ b/roles/totpcgi/files/html/error.html @@ -0,0 +1,26 @@ + + + + + Fedora Project Google Authenticator provisioning - Error + + + +
+
Fedora Project - Error
+
+

+ $errormsg +

+

+ You can try again or you can contact + the Fedora Infrastructure team at admin@fedoraproject.org. +

+
+ +
+ + diff --git a/roles/totpcgi/files/html/login.html b/roles/totpcgi/files/html/login.html new file mode 100644 index 0000000000..024a8ac84d --- /dev/null +++ b/roles/totpcgi/files/html/login.html @@ -0,0 +1,49 @@ + + + + + Fedora Project Google Authenticator provisioning + + + +
+
Fedora Project
+
+
+ Please log in to obtain your new Google Authenticator + secret. +
+
+ + + + + + + + + + + + + +
+ + + +
+ + + +
+ +
+
+
+ +
+ + diff --git a/roles/totpcgi/files/html/totp.html b/roles/totpcgi/files/html/totp.html new file mode 100644 index 0000000000..9e968e2ebd --- /dev/null +++ b/roles/totpcgi/files/html/totp.html @@ -0,0 +1,53 @@ + + + + + Fedora Project Google Authenticator Provisioning + + + + +
+
Fedora Project Google Authenticator Provisioning
+
+
+ $qrcode_embed +
+
+

+ Your new Google Authenticator token has been issued. + To import this token into your device, simply go to your + Google Authenticator app, select the option to add an + account, and then select "Scan Barcode". Point the camera + at the QR Barcode displayed next to this message. Google + Authenticator will then import your new token into the + device. It should be ready to use immediately. +

+
+
+

+ If the administrator permitted the use of scratch tokens, + you should see them listed below. If you lose access to + your Google Authenticator device, you should be able to + use one of these tokens to gain emergency access to your + account. Please write them down. +

+
+ $scratch_tokens +
+
+
+

+ If you require any help with your Google Authenticator + token or experience any difficulty importing it into + your mobile device, please email + admin@fedoraproject.org. +

+
+ +
+ + diff --git a/roles/totpcgi/files/index.cgi b/roles/totpcgi/files/index.cgi new file mode 100644 index 0000000000..fce10d6228 --- /dev/null +++ b/roles/totpcgi/files/index.cgi @@ -0,0 +1,189 @@ +#!/usr/bin/python -tt +## +# Copyright (C) 2012 by Konstantin Ryabitsev and contributors +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA +# 02111-1307, USA. +# +import os +import re +import sys +import cgi +import syslog +import logging +import urllib2 + +import cgitb +cgitb.enable() + +import totpcgi +import totpcgi.backends + +if len(sys.argv) > 1: + # blindly assume it's the config file + config_file = sys.argv[1] +else: + config_file = '/etc/totpcgi/totpcgi.conf' + +import ConfigParser + +from fedora.client import AuthError +from fedora.client.fasproxy import FasProxyClient + +config = ConfigParser.RawConfigParser() +config.read(config_file) + +require_pincode = config.getboolean('main', 'require_pincode') +success_string = config.get('main', 'success_string') + +fas_url = config.get('main', 'fas_url') +try: + fas = FasProxyClient(fas_url) +except Exception, e: + syslog.syslog(syslog.LOG_CRIT, 'Problem connecting to fas %s' % e) + sys.exit(1) + +backends = totpcgi.backends.Backends() + +try: + backends.load_from_config(config) +except totpcgi.backends.BackendNotSupported, ex: + syslog.syslog(syslog.LOG_CRIT, + 'Backend engine not supported: %s' % ex) + sys.exit(1) + +syslog.openlog('totp.cgi', syslog.LOG_PID, syslog.LOG_AUTH) + +### Begin custom Fedora Functions + +def google_auth_fas_pincode_verify(user, pincode): + if not fas.verify_password(user, pincode): + raise totpcgi.UserPincodeError('User Password Error') + +backends.pincode_backend.verify_user_pincode = google_auth_fas_pincode_verify + +client_id = '1' + +def parse_token(token): + if token > 44: + otp = token[-44:] + if otp.startswith('ccccc'): + return token[:-44], otp + + # Not a password + yubikey + return False + +class YubikeyAuthenticator(object): + auth_regex = re.compile('^status=(?P\w{2})') + def __init__(self, require_pincode=False): + self.require_pincode = require_pincode + + def verify_user_token(self, user, token): + # Parse the token apart into a password and token + password, otp = parse_token(token) + + # Verify token against yubikey server + server_prefix = 'http://localhost/yk-val/verify?id=' + server_url = server_prefix + client_id + "&otp=" + otp + + fh = urllib2.urlopen(server_url) + + for line in fh: + match = self.auth_regex.search(line.strip('\n')) + if match: + if match.group('rc') == 'OK': + # Yubikey token is valid + break + raise totpcgi.VerifyFailed(line.split('=')[1]) + else: + raise totpcgi.VerifyFailed('yk-val returned malformed response') + + + # Verify that the yubikey token belongs to the user + # As a side effect, verify the password is good as well + # if the user+password are wrong, this will raise a fedora.client.AuthError + try: + response = fas.send_request('/config/list/%s/yubikey' % user, + auth_params={'username': user, 'password': password}) + except AuthError, e: + raise totpcgi.VerifyFailed('User Password Error: %s' % e) + if not response[1].configs.prefix or not response[1].configs.enabled: + raise totpcgi.VerifyFailed('Yubikey OTP unconfigured') + elif len(response[1].configs.prefix) != 12: + raise totpcgi.VerifyFailed('Invalid Yubikey OTP prefix') + if not otp.startswith(response[1].configs.prefix): + raise totpcgi.VerifyFailed('Unauthorized/Invalid OTP') + + # Okay, everything passed + return 'Valid yubikey returned' + + +### End of custom Fedora Functions + +def bad_request(why): + output = 'ERR\n' + why + '\n' + sys.stdout.write('Status: 400 BAD REQUEST\n') + sys.stdout.write('Content-type: text/plain\n') + sys.stdout.write('Content-Length: %s\n' % len(output)) + sys.stdout.write('\n') + + sys.stdout.write(output) + sys.exit(0) + +def cgimain(): + form = cgi.FieldStorage() + + must_keys = ('user', 'token', 'mode') + + for must_key in must_keys: + if must_key not in form: + bad_request("Missing field: %s" % must_key) + + user = form.getfirst('user') + token = form.getfirst('token') + mode = form.getfirst('mode') + + remote_host = os.environ['REMOTE_ADDR'] + + if mode != 'PAM_SM_AUTH': + bad_request('We only support PAM_SM_AUTH') + + if parse_token(token): + ga = YubikeyAuthenticator(require_pincode) + else: + # totp/googleauth + ga = totpcgi.GoogleAuthenticator(backends, require_pincode) + + try: + status = ga.verify_user_token(user, token) + except Exception, ex: + syslog.syslog(syslog.LOG_NOTICE, + 'Failure: user=%s, mode=%s, host=%s, message=%s' % (user, mode, + remote_host, str(ex))) + bad_request(str(ex)) + + syslog.syslog(syslog.LOG_NOTICE, + 'Success: user=%s, mode=%s, host=%s, message=%s' % (user, mode, + remote_host, status)) + + sys.stdout.write('Status: 200 OK\n') + sys.stdout.write('Content-type: text/plain\n') + sys.stdout.write('Content-Length: %s\n' % len(success_string)) + sys.stdout.write('\n') + + sys.stdout.write(success_string) + +if __name__ == '__main__': + cgimain() diff --git a/roles/totpcgi/files/pam_url.conf b/roles/totpcgi/files/pam_url.conf new file mode 100644 index 0000000000..9ce7690b81 --- /dev/null +++ b/roles/totpcgi/files/pam_url.conf @@ -0,0 +1,21 @@ +pam_url: +{ + settings: + { + url = "https://fas-all.phx2.fedoraproject.org:8443/"; # URI to fetch + returncode = "OK"; # The remote script/cgi should return a 200 http code and this string as its only results + userfield = "user"; # userfield name to send + passwdfield = "token"; # passwdfield name to send + extradata = "&do=login"; # extradata to send + prompt = "Password+Token: "; # password prompt + }; + + ssl: + { + verify_peer = true; # Should we verify SSL ? + verify_host = true; # Should we verify the CN in the SSL cert? + client_cert = "/etc/pki/tls/private/totpcgi.pem"; # file to use as client-side certificate + client_key = "/etc/pki/tls/private/totpcgi.pem"; # file to use as client-side key (can be same file as above if a single cert) + ca_cert = "/etc/pki/tls/private/totpcgi-ca.cert"; + }; +}; diff --git a/roles/totpcgi/files/pam_url.conf.fakefas01 b/roles/totpcgi/files/pam_url.conf.fakefas01 new file mode 100644 index 0000000000..9ce7690b81 --- /dev/null +++ b/roles/totpcgi/files/pam_url.conf.fakefas01 @@ -0,0 +1,21 @@ +pam_url: +{ + settings: + { + url = "https://fas-all.phx2.fedoraproject.org:8443/"; # URI to fetch + returncode = "OK"; # The remote script/cgi should return a 200 http code and this string as its only results + userfield = "user"; # userfield name to send + passwdfield = "token"; # passwdfield name to send + extradata = "&do=login"; # extradata to send + prompt = "Password+Token: "; # password prompt + }; + + ssl: + { + verify_peer = true; # Should we verify SSL ? + verify_host = true; # Should we verify the CN in the SSL cert? + client_cert = "/etc/pki/tls/private/totpcgi.pem"; # file to use as client-side certificate + client_key = "/etc/pki/tls/private/totpcgi.pem"; # file to use as client-side key (can be same file as above if a single cert) + ca_cert = "/etc/pki/tls/private/totpcgi-ca.cert"; + }; +}; diff --git a/roles/totpcgi/files/pam_url.conf.stg b/roles/totpcgi/files/pam_url.conf.stg new file mode 100644 index 0000000000..508c5d9cf7 --- /dev/null +++ b/roles/totpcgi/files/pam_url.conf.stg @@ -0,0 +1,21 @@ +pam_url: +{ + settings: + { + url = "https://fas-all.stg.phx2.fedoraproject.org:8443/"; # URI to fetch + returncode = "OK"; # The remote script/cgi should return a 200 http code and this string as its only results + userfield = "user"; # userfield name to send + passwdfield = "token"; # passwdfield name to send + extradata = "&do=login"; # extradata to send + prompt = "Password+Token: "; # password prompt + }; + + ssl: + { + verify_peer = true; # Should we verify SSL ? + verify_host = true; # Should we verify the CN in the SSL cert? + client_cert = "/etc/pki/tls/private/totpcgi.pem"; # file to use as client-side certificate + client_key = "/etc/pki/tls/private/totpcgi.pem"; # file to use as client-side key (can be same file as above if a single cert) + ca_cert = "/etc/pki/tls/private/totpcgi-ca.cert"; + }; +}; diff --git a/roles/totpcgi/files/pam_url.conf.vpn b/roles/totpcgi/files/pam_url.conf.vpn new file mode 100644 index 0000000000..6e102e12f7 --- /dev/null +++ b/roles/totpcgi/files/pam_url.conf.vpn @@ -0,0 +1,21 @@ +pam_url: +{ + settings: + { + url = "https://fas-all.vpn.fedoraproject.org:8443/"; # URI to fetch + returncode = "OK"; # The remote script/cgi should return a 200 http code and this string as its only results + userfield = "user"; # userfield name to send + passwdfield = "token"; # passwdfield name to send + extradata = "&do=login"; # extradata to send + prompt = "Password+Token: "; # password prompt + }; + + ssl: + { + verify_peer = true; # Should we verify SSL ? + verify_host = true; # Should we verify the CN in the SSL cert? + client_cert = "/etc/pki/tls/private/totpcgi.pem"; # file to use as client-side certificate + client_key = "/etc/pki/tls/private/totpcgi.pem"; # file to use as client-side key (can be same file as above if a single cert) + ca_cert = "/etc/pki/tls/private/totpcgi-ca.cert"; + }; +}; diff --git a/roles/totpcgi/files/provisioning.cgi b/roles/totpcgi/files/provisioning.cgi new file mode 100644 index 0000000000..220493742c --- /dev/null +++ b/roles/totpcgi/files/provisioning.cgi @@ -0,0 +1,265 @@ +#!/usr/bin/python -tt +## +# Copyright (C) 2012 by Konstantin Ryabitsev and contributors +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA +# 02111-1307, USA. +# +import os +import sys +import cgi +import syslog +import logging + +import cgitb +cgitb.enable() + +import totpcgi +import totpcgi.backends +import totpcgi.utils + +import qrcode +from qrcode.image import svg +from StringIO import StringIO + +from string import Template + +if len(sys.argv) > 1: + # blindly assume it's the config file + config_file = sys.argv[1] +else: + config_file = '/etc/totpcgi/provisioning.conf' + +import ConfigParser + +config = ConfigParser.RawConfigParser() +config.read(config_file) + +backends = totpcgi.backends.Backends() + +try: + backends.load_from_config(config) +except totpcgi.backends.BackendNotSupported, ex: + syslog.syslog(syslog.LOG_CRIT, + 'Backend engine not supported: %s' % ex) + sys.exit(1) + +syslog.openlog('provisioning.cgi', syslog.LOG_PID, syslog.LOG_AUTH) + +def bad_request(config, why): + templates_dir = config.get('secret', 'templates_dir') + fh = open(os.path.join(templates_dir, 'error.html')) + tpt = Template(fh.read()) + fh.close() + + vals = { + 'action_url': config.get('secret', 'action_url'), + 'css_root': config.get('secret', 'css_root'), + 'errormsg': cgi.escape(why) + } + + out = tpt.safe_substitute(vals) + + sys.stdout.write('Status: 400 BAD REQUEST\n') + sys.stdout.write('Content-type: text/html\n') + sys.stdout.write('Content-Length: %s\n' % len(out)) + sys.stdout.write('\n') + + sys.stdout.write(out) + sys.exit(0) + +def show_qr_code(data): + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_L, + box_size=5, + border=4) + + qr.add_data(data) + qr.make(fit=True) + + img = qr.make_image() + + fh = StringIO() + img.save(fh) + out = fh.getvalue() + fh.close() + + sys.stdout.write('Status: 200 OK\n') + sys.stdout.write('Content-type: image/png\n') + sys.stdout.write('Content-Length: %s\n' % len(out)) + sys.stdout.write('\n') + + sys.stdout.write(out) + sys.exit(0) + +def show_login_form(config): + templates_dir = config.get('secret', 'templates_dir') + fh = open(os.path.join(templates_dir, 'login.html')) + tpt = Template(fh.read()) + fh.close() + + vals = { + 'action_url': config.get('secret', 'action_url'), + 'css_root': config.get('secret', 'css_root') + } + + out = tpt.safe_substitute(vals) + + sys.stdout.write('Status: 200 OK\n') + sys.stdout.write('Content-type: text/html\n') + sys.stdout.write('Content-Length: %s\n' % len(out)) + sys.stdout.write('\n') + + sys.stdout.write(out) + sys.exit(0) + +def show_totp_page(config, user, gaus): + # generate provisioning URI + tpt = Template(config.get('secret', 'totp_user_mask')) + totp_user = tpt.safe_substitute(username=user) + totp_qr_uri = gaus.totp.provisioning_uri(totp_user) + + action_url = config.get('secret', 'action_url') + + qrcode_embed = '' % (action_url, totp_qr_uri) + + templates_dir = config.get('secret', 'templates_dir') + fh = open(os.path.join(templates_dir, 'totp.html')) + tpt = Template(fh.read()) + fh.close() + + if gaus.scratch_tokens: + scratch_tokens = '
'.join(gaus.scratch_tokens) + else: + scratch_tokens = ' ' + + vals = { + 'action_url': action_url, + 'css_root': config.get('secret', 'css_root'), + 'qrcode_embed': qrcode_embed, + 'scratch_tokens': scratch_tokens + } + + out = tpt.safe_substitute(vals) + + sys.stdout.write('Status: 200 OK\n') + sys.stdout.write('Content-type: text/html\n') + sys.stdout.write('Content-Length: %s\n' % len(out)) + sys.stdout.write('\n') + + sys.stdout.write(out) + sys.exit(0) + +def generate_secret(config): + encrypt_secret = config.getboolean('secret', 'encrypt_secret') + window_size = config.getint('secret', 'window_size') + rate_limit = config.get('secret', 'rate_limit') + + # scratch tokens don't make any sense with encrypted secret + if not encrypt_secret: + scratch_tokens_n = config.getint('secret', 'scratch_tokens_n') + else: + scratch_tokens_n = 0 + + (times, secs) = rate_limit.split(',') + rate_limit = (int(times), int(secs)) + + gaus = totpcgi.utils.generate_secret(rate_limit, window_size, + scratch_tokens_n) + + return gaus + + +def cgimain(): + form = cgi.FieldStorage() + + if 'qrcode' in form: + #if os.environ['HTTP_REFERER'].find(os.environ['SERVER_NAME']) == -1: + # bad_request(config, 'Sorry, you failed the HTTP_REFERER check') + + qrcode = form.getfirst('qrcode') + show_qr_code(qrcode) + + remote_host = os.environ['REMOTE_ADDR'] + + try: + trust_http_auth = config.getboolean('secret', 'trust_http_auth') + except ConfigParser.NoOptionError: + trust_http_auth = False + + if trust_http_auth and os.environ.has_key('REMOTE_USER'): + user = os.environ['REMOTE_USER'] + pincode = None + + syslog.syslog(syslog.LOG_NOTICE, + 'Success (http-auth): user=%s, host=%s' % (user, remote_host)) + + else: + must_keys = ('username', 'pincode') + + for must_key in must_keys: + if must_key not in form: + show_login_form(config) + + user = form.getfirst('username') + pincode = form.getfirst('pincode') + + # start by verifying the pincode + try: + backends.pincode_backend.verify_user_pincode(user, pincode) + except Exception, ex: + syslog.syslog(syslog.LOG_NOTICE, + 'Failure: user=%s, host=%s, message=%s' % (user, remote_host, + str(ex))) + bad_request(config, str(ex)) + + # pincode verified + syslog.syslog(syslog.LOG_NOTICE, + 'Success: user=%s, host=%s' % (user, remote_host)) + + # is there an existing secret for this user? + exists = True + + try: + backends.secret_backend.get_user_secret(user, pincode) + except totpcgi.UserNotFound: + # if we got it, then there isn't an existing secret in place + exists = False + + if exists: + syslog.syslog(syslog.LOG_NOTICE, + 'Secret exists: user=%s, host=%s' % (user, remote_host)) + bad_request(config, 'Existing secret found. It must be removed first.') + + # now generate the secret and store it + + gaus = generate_secret(config) + + # if we don't need to encrypt the secret, set pincode to None + encrypt_secret = config.getboolean('secret', 'encrypt_secret') + if not encrypt_secret: + pincode = None + + backends.secret_backend.save_user_secret(user, gaus, pincode) + # purge all old state, as it's now obsolete + + backends.state_backend.delete_user_state(user) + + show_totp_page(config, user, gaus) + + +if __name__ == '__main__': + cgimain() diff --git a/roles/totpcgi/files/sudo.pam b/roles/totpcgi/files/sudo.pam new file mode 100644 index 0000000000..aa59ebf7a7 --- /dev/null +++ b/roles/totpcgi/files/sudo.pam @@ -0,0 +1,11 @@ +#%PAM-1.0 +auth required pam_env.so +auth sufficient pam_url.so config=/etc/pam_url.conf +auth requisite pam_succeed_if.so uid >= 500 quiet +auth required pam_deny.so + +auth include system-auth +account include system-auth +password include system-auth +session optional pam_keyinit.so revoke +session required pam_limits.so diff --git a/roles/totpcgi/files/sudo.pam.dev.fedoraproject.org b/roles/totpcgi/files/sudo.pam.dev.fedoraproject.org new file mode 100644 index 0000000000..030bb26463 --- /dev/null +++ b/roles/totpcgi/files/sudo.pam.dev.fedoraproject.org @@ -0,0 +1,6 @@ +#%PAM-1.0 +auth include system-auth +account include system-auth +password include system-auth +session optional pam_keyinit.so revoke +session required pam_limits.so diff --git a/roles/totpcgi/files/sudo.pam.qa.fedoraproject.org b/roles/totpcgi/files/sudo.pam.qa.fedoraproject.org new file mode 100644 index 0000000000..030bb26463 --- /dev/null +++ b/roles/totpcgi/files/sudo.pam.qa.fedoraproject.org @@ -0,0 +1,6 @@ +#%PAM-1.0 +auth include system-auth +account include system-auth +password include system-auth +session optional pam_keyinit.so revoke +session required pam_limits.so diff --git a/roles/totpcgi/files/totpcgi-httpd.conf b/roles/totpcgi/files/totpcgi-httpd.conf new file mode 100644 index 0000000000..bfcaca319c --- /dev/null +++ b/roles/totpcgi/files/totpcgi-httpd.conf @@ -0,0 +1,34 @@ +Listen 8443 + + # Load this module locally here. + LoadModule suexec_module modules/mod_suexec.so + + ServerAdmin admin@fedoraproject.org + DocumentRoot /var/www/totpcgi + ServerName fas-all.phx2.fedoraproject.org:8443 + ErrorLog /var/log/httpd/totpcgi-error.log + SuexecUserGroup totpcgi totpcgi + + # Use this for totp.cgi + AddHandler cgi-script .cgi + DirectoryIndex index.cgi + + # Or use this for totp.fcgi: + #AddHandler fcgid-script .fcgi + #DirectoryIndex index.fcgi + + SSLEngine on + SSLCertificateFile /etc/pki/totpcgi/totpcgi-server.crt + SSLCertificateKeyFile /etc/pki/totpcgi/totpcgi-server.key + SSLCACertificateFile /etc/pki/totpcgi/totpcgi-ca.crt + + SSLVerifyClient require + SSLVerifyDepth 10 + + CustomLog /var/log/httpd/totpcgi-ssl-request-log \ + "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b" + + + Options ExecCGI + + diff --git a/roles/totpcgi/files/totpcgi-httpd.conf.stg b/roles/totpcgi/files/totpcgi-httpd.conf.stg new file mode 100644 index 0000000000..87ad5a7492 --- /dev/null +++ b/roles/totpcgi/files/totpcgi-httpd.conf.stg @@ -0,0 +1,34 @@ +Listen 8443 + + # Load this module locally here. + LoadModule suexec_module modules/mod_suexec.so + + ServerAdmin admin@fedoraproject.org + DocumentRoot /var/www/totpcgi + ServerName fas-all.stg.phx2.fedoraproject.org:8443 + ErrorLog /var/log/httpd/totpcgi-error.log + SuexecUserGroup totpcgi totpcgi + + # Use this for totp.cgi + AddHandler cgi-script .cgi + DirectoryIndex index.cgi + + # Or use this for totp.fcgi: + #AddHandler fcgid-script .fcgi + #DirectoryIndex index.fcgi + + SSLEngine on + SSLCertificateFile /etc/pki/totpcgi/totpcgi-server.crt + SSLCertificateKeyFile /etc/pki/totpcgi/totpcgi-server.key + SSLCACertificateFile /etc/pki/totpcgi/totpcgi-ca.crt + + SSLVerifyClient require + SSLVerifyDepth 10 + + CustomLog /var/log/httpd/totpcgi-ssl-request-log \ + "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b" + + + Options ExecCGI + + diff --git a/roles/totpcgi/files/totpcgi-httpd.conf.vpn b/roles/totpcgi/files/totpcgi-httpd.conf.vpn new file mode 100644 index 0000000000..55ff5b088e --- /dev/null +++ b/roles/totpcgi/files/totpcgi-httpd.conf.vpn @@ -0,0 +1,33 @@ + + # Load this module locally here. + LoadModule suexec_module modules/mod_suexec.so + + ServerAdmin admin@fedoraproject.org + DocumentRoot /var/www/totpcgi + ServerName fas-all.vpn.fedoraproject.org:8443 + ErrorLog /var/log/httpd/totpcgi-error.log + SuexecUserGroup totpcgi totpcgi + + # Use this for totp.cgi + AddHandler cgi-script .cgi + DirectoryIndex index.cgi + + # Or use this for totp.fcgi: + #AddHandler fcgid-script .fcgi + #DirectoryIndex index.fcgi + + SSLEngine on + SSLCertificateFile /etc/pki/totpcgi/totpcgi-server-vpn.crt + SSLCertificateKeyFile /etc/pki/totpcgi/totpcgi-server-vpn.key + SSLCACertificateFile /etc/pki/totpcgi/totpcgi-ca.crt + + SSLVerifyClient require + SSLVerifyDepth 10 + + CustomLog /var/log/httpd/totpcgi-ssl-request-log \ + "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b" + + + Options ExecCGI + + diff --git a/roles/totpcgi/tasks/main.yml b/roles/totpcgi/tasks/main.yml new file mode 100644 index 0000000000..66ec18b006 --- /dev/null +++ b/roles/totpcgi/tasks/main.yml @@ -0,0 +1,220 @@ +- name: install needed packages + yum: pkg={{ item }} state=present + with_items: + - mod_auth_psql + - totpcgi + - totpcgi-selinux + - totpcgi-provisioning + - python-qrcode + - httpd + - mod_ssl + tags: + - packages + +- name: add totpcgi user + user: name=totpcgi uid=430 state=present home=/var/lib/totpcgi createhome=yes system=yes + tags: + - config + +- name: Install the cgi apache configuration files + template: > + src={{ item }}.j2 dest=/etc/httpd/conf.d/{{ item }} + owner=root group=root mode=0444 + with_items: + - provisioning-httpd.conf + tags: + - files + - config + notify: + - restart apache + +- name: create directories + file: path=/etc/{{ item.path }} state=directory owner=root group=totpcgi mode=750 + with_items: + - pki/totpcgi + - totpcgi + - totpcgi/templates + - totpcgi/totp + +- name: copy index file over + copy: > + src=html + dest=/etc/totpcgi/templates/html + owner=root + group=totpcgiprov + mode=0750 + tags: + - files + - config + +- name: copy index file over + copy: > + src=provisioning.cgi + dest=/var/www/totpcgi-provisioning/index.cgi + owner=totpcgiprov + group=totpcgiprov + mode=0550 + tags: + - files + - config + +- name: copy index file over + copy: > + src=index.cgi + dest=/var/www/totpcgi/index.cgi + owner=totpcgiprov + group=totpcgiprov + mode=0550 + tags: + - files + - config + +- name: copy totpcgi.conf file over + template: > + src=totpcgi.conf.j2 + dest=/etc/totpcgi/totpcgi.conf + owner=root + group=totpcgiprov + mode=0640 + tags: + - files + - config + +# staging certs + +- name: copy server cert file over + copy: > + src={{ puppet_secure }}/2fa-certs/keys/fas-all.stg.phx2.fedoraproject.org.crt + dest=/etc/pki/totpcgi/totpcgi-server.crt + owner=root + group=totpcgi + mode=0640 + tags: + - files + - config + when: env == "staging" + +- name: copy server cert file over + copy: > + src={{ puppet_secure }}/fa-certs/keys/fas-all.stg.phx2.fedoraproject.org.key + dest=/etc/pki/totpcgi/totpcgi-server.key + owner=root + group=totpcgi + mode=0640 + tags: + - files + - config + when: env == "staging" + +- name: copy server cert file over + copy: > + src=totpcgi-httpd.conf.stg + dest=/etc/httpd/conf.d/totpcgi.conf + owner=root + group=root + mode=0444 + tags: + - files + - config + when: env == "staging" + +# prod certs + +- name: copy server cert file over + copy: > + src={{ puppet_secure }}/2fa-certs/keys/fas-all.phx2.fedoraproject.org.crt + dest=/etc/pki/totpcgi/totpcgi-server.crt + owner=root + group=totpcgi + mode=0640 + tags: + - files + - config + when: env == "production" + +- name: copy server cert file over + copy: > + src={{ puppet_secure }}/fa-certs/keys/fas-all.phx2.fedoraproject.org.key + dest=/etc/pki/totpcgi/totpcgi-server.key + owner=root + group=totpcgi + mode=0640 + tags: + - files + - config + when: env == "production" + +- name: copy server cert file over + copy: > + src=totpcgi-httpd.conf + dest=/etc/httpd/conf.d/totpcgi.conf + owner=root + group=root + mode=0444 + tags: + - files + - config + when: env == "production" + +# vpn certs + +- name: copy server cert file over + copy: > + src={{ puppet_secure }}/2fa-certs/keys/fas-all.phx2.fedoraproject.org.crt + dest=/etc/pki/totpcgi/totpcgi-server.crt + owner=root + group=totpcgi + mode=0640 + tags: + - files + - config + when: env == "production" + +- name: copy server cert file over + copy: > + src={{ puppet_secure }}/fa-certs/keys/fas-all.phx2.fedoraproject.org.key + dest=/etc/pki/totpcgi/totpcgi-server.key + owner=root + group=totpcgi + mode=0640 + tags: + - files + - config + when: env == "production" + +- name: copy server cert file over + copy: > + src=totpcgi-httpd.conf + dest=/etc/httpd/conf.d/totpcgi.conf + owner=root + group=root + mode=0444 + tags: + - files + - config + when: env == "production" +# +# TODO: vpn certs +# + +- name: copy server cert file over + copy: > + src={{ puppet_private }}/2fa-certs/keys/ca.crt + dest=/etc/pki/totpcgi/totpcgi-ca.crt + owner=root + group=totpcgi + mode=0640 + tags: + - files + - config + +- name: copy server cert file over + template: > + src=provisioning.conf.j2 + dest=/etc/totpcgi/provisioning.conf + owner=root + group=totpcgiprov + mode=0640 + tags: + - files + - config diff --git a/roles/totpcgi/templates/provisioning-httpd.conf.j2 b/roles/totpcgi/templates/provisioning-httpd.conf.j2 new file mode 100644 index 0000000000..102ee3255f --- /dev/null +++ b/roles/totpcgi/templates/provisioning-httpd.conf.j2 @@ -0,0 +1,44 @@ +Listen 8444 + + LoadModule suexec_module modules/mod_suexec.so + + DocumentRoot /var/www/totpcgi-provisioning + ServerName fas01.stg.phx2.fedoraproject.org:8444 + ErrorLog /var/log/httpd/totpcgi-provisioning-error.log + SuexecUserGroup totpcgiprov totpcgiprov + + AddHandler cgi-script .cgi + DirectoryIndex index.cgi + + Header set Cache-Control no-cache + Header set Expires 0 + + #SSLEngine on + #SSLCertificateFile /etc/pki/totpcgi/totpcgi-server.crt + #SSLCertificateKeyFile /etc/pki/totpcgi/totpcgi-server.key + #SSLCACertificateFile /etc/pki/totpcgi/totpcgi-ca.crt + + #CustomLog /var/log/httpd/totpcgi-provisioning-ssl-request-log \ + # "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b" + + + Options ExecCGI + + + + AuthType Basic + AuthName "Fedora totpcgi" + + Auth_PG_host db-fas + Auth_PG_port 5432 + Auth_PG_user fasreadonly + Auth_PG_pwd {{ fasReadOnlyPassword }} + Auth_PG_database fas2 + Auth_PG_pwd_table people + Auth_PG_uid_field username + Auth_PG_pwd_field password + Auth_PG_pwd_whereclause " and status='active'" + + Require valid-user + + diff --git a/roles/totpcgi/templates/provisioning.conf.j2 b/roles/totpcgi/templates/provisioning.conf.j2 new file mode 100644 index 0000000000..ae963bc632 --- /dev/null +++ b/roles/totpcgi/templates/provisioning.conf.j2 @@ -0,0 +1,88 @@ +[secret] +# Whether to encrypt the secret when we generate it. Encrypting the secret +# with the user's pincode means that even if the .totp file is leaked, an +# attacker will not be able to get the secret without knowing the user's +# pincode. The downside is that if a user forgets their pincode, both the +# pincode and the secret will need to be fully re-provisioned. +# Setting to "True" will also turn off scratch-token support. +encrypt_secret = False + +# You can allow for some clock drift between the client and server by setting +# the permitted window size. Window size is calculated in 10-second intervals, +# so a window size of 6 allows clock drift of 60 seconds in either direction. +window_size = 3 + +# First value is the number of times. Second value is the number of seconds. +# So, "3, 30" means "3 falures within 30 seconds" +rate_limit = 3, 30 + +# How many scratch tokens to generate. Note, that this setting is ignored +# if encrypt_secret is set to True. +scratch_tokens_n = 5 + +# This identifies the token in the Google Authenticator application. It comes +# very handy when users have more than one token, so set this to something +# descriptive of your environment. +{% if environment == "staging" %} +totp_user_mask = $username@stg.fedoraproject.org +{% else %} +totp_user_mask = $username@fedoraproject.org +{% endif %} + +# Used by provisioning.cgi +# Where the provisioning CGI is located, with regards to the web root. +action_url = /totpcgiprovision/index.cgi + +# Used by provisioning.cgi +# Where provisioning.css and provisioning-print.css are located with regards +# to the web root. +css_root = /totpcgiprovision/ + +# Used by provisioning.cgi +# Where to find the templates files. +templates_dir = /etc/totpcgi/templates + +# Used by provisioning.cgi +# Whether to rely on HTTP auth to handle authentication. +# As we don't get the password, only the username, turning this on +# will automatically set encrypt_secret to false. +# +# Be careful turning this on. +trust_http_auth = True + + +[pincode] +# Which hashing mechanism to use. Valid entries: md5, bcrypt, sha256, sha512 +usehash = sha256 + +# Whether to compile the DBM database (only meaningful with the file backend) +makedb = True + +# The backends are pretty much the same as in totpcgi.conf, except if you +# are using the postgresql secret backend, you need to connect as a user +# that is allowed to modify user records (e.g. totpcgi_admin). +[secret_backend] +;engine = file +;secrets_dir = /etc/totpcgi/totp + +; For PostgreSQL backend: +engine = pgsql +pg_connect_string = user={{ totpcgiadminDBUser }} password={{ totpcgiadminDBPassword }} host=db-fas01 dbname=totpcgi + +[pincode_backend] +engine = pgsql +pg_connect_string = user={{ totpcgiadminDBUser }} password={{ totpcgiadminDBPassword }} host=db-fas01 dbname=totpcgi + +; For LDAP backend (simple bind auth): +;engine = ldap +;ldap_url = ldaps://ipa.example.com:636/ +;ldap_dn = uid=$username,cn=users,cn=accounts,dc=example,dc=com +;ldap_cacert = /etc/pki/tls/certs/ipa-ca.crt + +[state_backend] +;engine = file +;state_dir = /var/lib/totpcgi + +; For PostgreSQL backend: +engine = pgsql +pg_connect_string = user={{ totpcgiadminDBUser }} password={{ totpcgiadminDBPassword }} host=db-fas01 dbname=totpcgi diff --git a/roles/totpcgi/templates/totpcgi.conf.j2 b/roles/totpcgi/templates/totpcgi.conf.j2 new file mode 100644 index 0000000000..1a5b9b945d --- /dev/null +++ b/roles/totpcgi/templates/totpcgi.conf.j2 @@ -0,0 +1,31 @@ +[main] +require_pincode = True +success_string = OK +{% if env == "staging" %} +fas_url = https://admin.stg.fedoraproject.org/accounts/ +{% else %} +fas_url = https://admin.fedoraproject.org/accounts/ +{% endif %} + +[secret_backend] +; For PostgreSQL backend: +engine = pgsql +pg_connect_string = user={{ totpcgiDBUser }} password={{ totpcgiDBPassword }} host=db-fas01 dbname=totpcgi + +[pincode_backend] +engine = pgsql +pg_connect_string = user={{ totpcgiDBUser }} password={{ totpcgiDBPassword }} host=db-fas01 dbname=totpcgi + +; For LDAP backend (simple bind auth): +;engine = ldap +;ldap_url = ldaps://ipa.example.com:636/ +;ldap_dn = uid=$username,cn=users,cn=accounts,dc=example,dc=com +;ldap_cacert = /etc/pki/tls/certs/ipa-ca.crt + +[state_backend] +;engine = file +;state_dir = /var/lib/totpcgi + +; For PostgreSQL backend: +engine = pgsql +pg_connect_string = user={{ totpcgiDBUser }} password={{ totpcgiDBPassword }} host=db-fas01 dbname=totpcgi