diff --git a/modules/openstack_project/files/irc/channels.yaml b/modules/openstack_project/files/irc/channels.yaml
new file mode 100644
index 0000000000..0492e420da
--- /dev/null
+++ b/modules/openstack_project/files/irc/channels.yaml
@@ -0,0 +1,45 @@
+# Copyright 2014 OpenStack Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Global definitions
+# First set up the access levels (map names in this file to chanserv flags):
+access:
+  masters: +AFRfiorstv
+  operators: +Aiortv
+  topics: +t
+
+# Define access that should apply to all channels:
+global:
+  masters:
+    - openstackinfra
+  operators:
+    - jeblair
+    - mtaylor
+    - clarkb
+    - fungi
+    - SergeyLukjanov
+    - ttx
+    - reed
+  topics:
+    - openstackstatus
+
+# Individual channel configuration:
+channels:
+  - name: openstack-infra
+  - name: openstack-meeting
+    topics:
+      - open_stack
+  - name: openstack-nova
+    operators:
+      - russellb
diff --git a/modules/openstack_project/files/irc/checkaccess.py b/modules/openstack_project/files/irc/checkaccess.py
new file mode 100644
index 0000000000..8e5575c0ea
--- /dev/null
+++ b/modules/openstack_project/files/irc/checkaccess.py
@@ -0,0 +1,144 @@
+#! /usr/bin/env python
+
+# Copyright 2011, 2013-2014 OpenStack Foundation
+# Copyright 2012 Hewlett-Packard Development Company, L.P.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import irc.client
+import logging
+import random
+import string
+import sys
+import yaml
+
+logging.basicConfig(level=logging.INFO)
+
+
+class CheckAccess(irc.client.SimpleIRCClient):
+    log = logging.getLogger("checkaccess")
+
+    def __init__(self, channels, nick, flags):
+        irc.client.SimpleIRCClient.__init__(self)
+        self.identify_msg_cap = False
+        self.channels = channels
+        self.nick = nick
+        self.flags = flags
+        self.current_channel = None
+        self.current_list = []
+        self.failed = True
+
+    def on_disconnect(self, connection, event):
+        if self.failed:
+            sys.exit(1)
+        else:
+            sys.exit(0)
+
+    def on_welcome(self, c, e):
+        self.identify_msg_cap = False
+        self.log.debug("Requesting identify-msg capability")
+        c.cap('REQ', 'identify-msg')
+        c.cap('END')
+
+    def on_cap(self, c, e):
+        self.log.debug("Received cap response %s" % repr(e.arguments))
+        if e.arguments[0] == 'ACK' and 'identify-msg' in e.arguments[1]:
+            self.log.debug("identify-msg cap acked")
+            self.identify_msg_cap = True
+            self.advance()
+
+    def on_privnotice(self, c, e):
+        if not self.identify_msg_cap:
+            self.log.debug("Ignoring message because identify-msg "
+                           "cap not enabled")
+            return
+        nick = e.source.split('!')[0]
+        auth = e.arguments[0][0]
+        msg = e.arguments[0][1:]
+        if auth != '+' or nick != 'ChanServ':
+            self.log.debug("Ignoring message from unauthenticated "
+                           "user %s" % nick)
+            return
+        self.failed = False
+        self.advance(msg)
+
+    def advance(self, msg=None):
+        if not self.current_channel:
+            if not self.channels:
+                self.connection.quit()
+                return
+            self.current_channel = self.channels.pop()
+            self.current_list = []
+            self.connection.privmsg('chanserv', 'access list %s' %
+                                    self.current_channel)
+            return
+        if msg.startswith('End of'):
+            found = False
+            for nick, flags, msg in self.current_list:
+                if nick == self.nick and flags == self.flags:
+                    self.log.info('%s access ok on %s' %
+                                  (self.nick, self.current_channel))
+                    found = True
+                    break
+            if not found:
+                self.failed = True
+                print ("%s does not have permissions on %s:" %
+                       (self.nick, self.current_channel))
+                for nick, flags, msg in self.current_list:
+                    print msg
+                print
+            self.current_channel = None
+            self.advance()
+            return
+        parts = msg.split()
+        self.current_list.append((parts[1], parts[2], msg))
+
+
+def main():
+    parser = argparse.ArgumentParser(description='IRC channel access check')
+    parser.add_argument('-l', dest='config',
+                        default='/etc/irc/channels.yaml',
+                        help='path to the config file')
+    parser.add_argument('-s', dest='server',
+                        default='chat.freenode.net',
+                        help='IRC server')
+    parser.add_argument('-p', dest='port',
+                        default=6667,
+                        help='IRC port')
+    parser.add_argument('nick',
+                        help='the nick for which access should be validated')
+    args = parser.parse_args()
+
+    config = yaml.load(open(args.config))
+    channels = []
+    for channel in config['channels']:
+        channels.append('#' + channel['name'])
+
+    access_level = None
+    for level, names in config['global'].items():
+        if args.nick in names:
+            access_level = level
+    if access_level is None:
+        raise Exception("Unable to determine global access level for %s" %
+                        args.nick)
+    flags = config['access'][access_level]
+
+    a = CheckAccess(channels, args.nick, flags)
+    mynick = ''.join(random.choice(string.ascii_uppercase)
+                     for x in range(16))
+    a.connect(args.server, int(args.port), mynick)
+    a.start()
+
+if __name__ == "__main__":
+    main()
diff --git a/modules/openstack_project/files/jenkins_job_builder/config/infra.yaml b/modules/openstack_project/files/jenkins_job_builder/config/infra.yaml
index f31cf7baaf..e47a44a2fe 100644
--- a/modules/openstack_project/files/jenkins_job_builder/config/infra.yaml
+++ b/modules/openstack_project/files/jenkins_job_builder/config/infra.yaml
@@ -10,6 +10,21 @@
       - console-log
 
 
+- job:
+    name: gate-config-irc-access
+    node: bare-precise
+
+    builders:
+      - gerrit-git-prep
+      - tox:
+          envlist: 'irc'
+          github-org: 'openstack-infra'
+          project: 'config'
+
+    publishers:
+      - console-log
+
+
 - job:
     name: gate-config-layout
     node: bare-precise
diff --git a/modules/openstack_project/files/zuul/layout.yaml b/modules/openstack_project/files/zuul/layout.yaml
index c8247a76e2..9f88e36cc0 100644
--- a/modules/openstack_project/files/zuul/layout.yaml
+++ b/modules/openstack_project/files/zuul/layout.yaml
@@ -410,6 +410,10 @@ jobs:
     voting: false
     failure-message: Jenkins XML output has changed.
     success-message: Jenkins XML output is unchanged.
+  - name: gate-config-irc-access
+    voting: false
+    files:
+      - 'modules/openstack_project/files/irc/channels.yaml'
 # Continous publishing from master of the following documentation targets:
   - name: openstack-admin-guide-cloud
     branch: ^master$
@@ -2590,6 +2594,7 @@ projects:
       - gate-config-pep8
       - gate-config-puppet-lint
       - gate-config-puppet-syntax
+      - gate-config-irc-access
       - gate-ci-docs
       - check-projects-yaml-alphabetized
     gate:
@@ -2597,6 +2602,7 @@ projects:
       - gate-config-pep8
       - gate-config-puppet-lint
       - gate-config-puppet-syntax
+      - gate-config-irc-access
       - check-projects-yaml-alphabetized
     post:
       - ci-docs
diff --git a/tox.ini b/tox.ini
index e137686c5a..23b8faef26 100644
--- a/tox.ini
+++ b/tox.ini
@@ -14,6 +14,11 @@ commands = flake8
 [testenv:venv]
 commands = {posargs}
 
+[testenv:irc]
+deps = PyYAML
+     irc
+commands = python modules/openstack_project/files/irc/checkaccess.py -l modules/openstack_project/files/irc/channels.yaml openstackinfra
+
 [flake8]
 show-source = True
 exclude = .tox