diff --git a/public/src/admin/manage/category.js b/public/src/admin/manage/category.js
index bc0b1652b5..866bc4bb9d 100644
--- a/public/src/admin/manage/category.js
+++ b/public/src/admin/manage/category.js
@@ -8,44 +8,9 @@ define('admin/manage/category', [
 	'autocomplete'
 ], function (uploader, iconSelect, colorpicker, autocomplete) {
 	var	Category = {};
+	var modified_categories = {};
 
 	Category.init = function () {
-		var modified_categories = {};
-
-		function modified(el) {
-			var cid = $(el).parents('form').attr('data-cid');
-
-			if (cid) {
-				modified_categories[cid] = modified_categories[cid] || {};
-				modified_categories[cid][$(el).attr('data-name')] = $(el).val();
-
-				app.flags = app.flags || {};
-				app.flags._unsaved = true;
-			}
-		}
-
-		function save(e) {
-			e.preventDefault();
-
-			if(Object.keys(modified_categories).length) {
-				socket.emit('admin.categories.update', modified_categories, function (err, result) {
-					if (err) {
-						return app.alertError(err.message);
-					}
-
-					if (result && result.length) {
-						app.flags._unsaved = false;
-						app.alert({
-							title: 'Updated Categories',
-							message: 'Category IDs ' + result.join(', ') + ' was successfully updated.',
-							type: 'success',
-							timeout: 2000
-						});
-					}
-				});
-				modified_categories = {};
-			}
-		}
 
 		$('.blockclass, form.category select').each(function () {
 			var $this = $(this);
@@ -85,7 +50,28 @@ define('admin/manage/category', [
 
 		$('[data-name="bgColor"], [data-name="color"]').each(enableColorPicker);
 
-		$('#save').on('click', save);
+		$('#save').on('click', function () {
+			if (Object.keys(modified_categories).length) {
+				socket.emit('admin.categories.update', modified_categories, function (err, result) {
+					if (err) {
+						return app.alertError(err.message);
+					}
+
+					if (result && result.length) {
+						app.flags._unsaved = false;
+						app.alert({
+							title: 'Updated Categories',
+							message: 'Category IDs ' + result.join(', ') + ' was successfully updated.',
+							type: 'success',
+							timeout: 2000
+						});
+					}
+				});
+				modified_categories = {};
+			}
+			return false;
+		});
+
 		$('.purge').on('click', function (e) {
 			e.preventDefault();
 
@@ -171,8 +157,37 @@ define('admin/manage/category', [
 		});
 
 		Category.setupPrivilegeTable();
+
+		handleTags();
 	};
 
+	function modified(el) {
+		var cid = ajaxify.data.category.cid;
+
+		if (cid) {
+			modified_categories[cid] = modified_categories[cid] || {};
+			modified_categories[cid][$(el).attr('data-name')] = $(el).val();
+
+			app.flags = app.flags || {};
+			app.flags._unsaved = true;
+		}
+	}
+
+	function handleTags() {
+		var tagEl = $('#tag-whitelist');
+		tagEl.tagsinput({
+			confirmKeys: [13, 44],
+			trimValue: true
+		});
+
+		ajaxify.data.category.tagWhitelist.forEach(function (tag) {
+			tagEl.tagsinput('add', tag);
+		});
+		tagEl.on('itemAdded itemRemoved', function (event) {
+			modified(tagEl);
+		});
+	}
+
 	Category.setupPrivilegeTable = function () {
 		$('.privilege-table-container').on('change', 'input[type="checkbox"]', function () {
 			var checkboxEl = $(this),
diff --git a/public/src/utils.js b/public/src/utils.js
index 0b78b34030..42d4b6f3ad 100644
--- a/public/src/utils.js
+++ b/public/src/utils.js
@@ -101,7 +101,7 @@
 		},
 
 		cleanUpTag: function (tag, maxLength) {
-			if (typeof tag !== 'string' || !tag.length ) {
+			if (typeof tag !== 'string' || !tag.length) {
 				return '';
 			}
 
diff --git a/src/categories.js b/src/categories.js
index 28d99a7d7f..4458e09481 100644
--- a/src/categories.js
+++ b/src/categories.js
@@ -141,6 +141,9 @@ var privileges = require('./privileges');
 			parents: function (next) {
 				Categories.getParents(cids, next);
 			},
+			tagWhitelist: function (next) {
+				Categories.getTagWhitelist(cids, next);
+			},
 			hasRead: function (next) {
 				Categories.hasReadCategories(cids, uid, next);
 			}
@@ -149,20 +152,26 @@ var privileges = require('./privileges');
 				return callback(err);
 			}
 
-			var categories = results.categories;
-			var hasRead = results.hasRead;
 			uid = parseInt(uid, 10);
-			for(var i = 0; i < results.categories.length; ++i) {
-				if (categories[i]) {
-					categories[i]['unread-class'] = (parseInt(categories[i].topic_count, 10) === 0 || (hasRead[i] && uid !== 0)) ? '' : 'unread';
-					categories[i].children = results.children[i];
-					categories[i].parent = results.parents[i] || undefined;
-					calculateTopicPostCount(categories[i]);
+			results.categories.forEach(function (category, i) {
+				if (category) {
+					category['unread-class'] = (parseInt(category.topic_count, 10) === 0 || (results.hasRead[i] && uid !== 0)) ? '' : 'unread';
+					category.children = results.children[i];
+					category.parent = results.parents[i] || undefined;
+					category.tagWhitelist = results.tagWhitelist[i];
+					calculateTopicPostCount(category);
 				}
-			}
+			});
+
+			callback(null, results.categories);
+		});
+	};
 
-			callback(null, categories);
+	Categories.getTagWhitelist = function (cids, callback) {
+		var keys = cids.map(function (cid) {
+			return 'cid:' + cid + ':tag:whitelist';
 		});
+		db.getSortedSetsMembers(keys, callback);
 	};
 
 	function calculateTopicPostCount(category) {
diff --git a/src/categories/delete.js b/src/categories/delete.js
index f91842e487..06d4931132 100644
--- a/src/categories/delete.js
+++ b/src/categories/delete.js
@@ -54,6 +54,7 @@ module.exports = function (Categories) {
 					'cid:' + cid + ':read_by_uid',
 					'cid:' + cid + ':ignorers',
 					'cid:' + cid + ':children',
+					'cid:' + cid + ':tag:whitelist',
 					'category:' + cid
 				], next);
 			},
diff --git a/src/categories/update.js b/src/categories/update.js
index 485d3e3834..afaad6d974 100644
--- a/src/categories/update.js
+++ b/src/categories/update.js
@@ -2,7 +2,9 @@
 'use strict';
 
 var async = require('async');
+
 var db = require('../database');
+var meta = require('../meta');
 var utils = require('../../public/src/utils');
 var translator = require('../../public/src/modules/translator');
 var plugins = require('../plugins');
@@ -66,6 +68,8 @@ module.exports = function (Categories) {
 	function updateCategoryField(cid, key, value, callback) {
 		if (key === 'parentCid') {
 			return updateParent(cid, value, callback);
+		} else if (key === 'tagWhitelist') {
+			return updateTagWhitelist(cid, value, callback);
 		}
 
 		async.waterfall([
@@ -112,6 +116,25 @@ module.exports = function (Categories) {
 		});
 	}
 
+	function updateTagWhitelist(cid, tags, callback) {
+		tags = tags.split(',');
+		tags = tags.map(function (tag) {
+			return utils.cleanUpTag(tag, meta.config.maximumTagLength);
+		}).filter(Boolean);
+
+		async.waterfall([
+			function (next) {
+				db.delete('cid:' + cid + ':tag:whitelist', next);
+			},
+			function (next) {
+				var scores = tags.map(function (tag, index) {
+					return index;
+				});
+				db.sortedSetAdd('cid:' + cid + ':tag:whitelist', scores, tags, next);
+			}
+		], callback);
+	}
+
 	function updateOrder(cid, order, callback) {
 		async.waterfall([
 			function (next) {
diff --git a/src/meta/css.js b/src/meta/css.js
index 583927e04a..b9bc1787b6 100644
--- a/src/meta/css.js
+++ b/src/meta/css.js
@@ -78,6 +78,7 @@ module.exports = function (Meta) {
 					acpSource += '\n@import "..' + path.sep + 'public/less/generics.less";\n';
 					acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/colorpicker/colorpicker.css";\n';
 					acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/jquery/css/smoothness/jquery-ui.css";';
+					acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";';
 
 					minify(acpSource, paths, 'acpCache', callback);
 				}
diff --git a/src/socket.io/topics/tags.js b/src/socket.io/topics/tags.js
index 67e48fcce8..8829229ca8 100644
--- a/src/socket.io/topics/tags.js
+++ b/src/socket.io/topics/tags.js
@@ -1,10 +1,29 @@
 'use strict';
 
 var async = require('async');
+var db = require('../../database');
 var topics = require('../../topics');
 var utils = require('../../../public/src/utils');
 
 module.exports = function (SocketTopics) {
+
+	SocketTopics.isTagAllowed = function (socket, data, callback) {
+		if (!data || !data.cid || !data.tag) {
+			return callback(new Error('[[error:invalid-data]]'));
+		}
+		async.waterfall([
+			function (next) {
+				db.getSortedSetRange('cid:' + data.cid + ':tag:whitelist', 0, -1, next);
+			},
+			function (tagWhitelist, next) {
+				if (!tagWhitelist.length) {
+					return next(null, true);
+				}
+				next(null, tagWhitelist.indexOf(data.tag) !== -1);
+			}
+		], callback);
+	};
+
 	SocketTopics.autocompleteTags = function (socket, data, callback) {
 		topics.autocompleteTags(data, callback);
 	};
diff --git a/src/topics/tags.js b/src/topics/tags.js
index a88c5119d5..3f6bfbab36 100644
--- a/src/topics/tags.js
+++ b/src/topics/tags.js
@@ -31,6 +31,10 @@ module.exports = function (Topics) {
 					return tag && tag.length >= (meta.config.minimumTagLength || 3) && array.indexOf(tag) === index;
 				});
 
+				filterCategoryTags(tags, tid, next);
+			},
+			function (_tags, next) {
+				tags = _tags;
 				var keys = tags.map(function (tag) {
 					return 'tag:' + tag + ':topics';
 				});
@@ -39,15 +43,35 @@ module.exports = function (Topics) {
 					async.apply(db.setAdd, 'topic:' + tid + ':tags', tags),
 					async.apply(db.sortedSetsAdd, keys, timestamp, tid)
 				], function (err) {
-					if (err) {
-						return next(err);
-					}
-					async.each(tags, updateTagCount, next);
+					next(err);
 				});
+			},
+			function (next) {
+				async.each(tags, updateTagCount, next);
 			}
 		], callback);
 	};
 
+	function filterCategoryTags(tags, tid, callback) {
+		async.waterfall([
+			function (next) {
+				Topics.getTopicField(tid, 'cid', next);
+			},
+			function (cid, next) {
+				db.getSortedSetRange('cid:' + cid + ':tag:whitelist', 0, -1, next);
+			},
+			function (tagWhitelist, next) {
+				if (!tagWhitelist.length) {
+					return next(null, tags);
+				}
+				tags = tags.filter(function (tag) {
+					return tagWhitelist.indexOf(tag) !== -1;
+				});
+				next(null, tags);
+			}
+		], callback);
+	}
+
 	Topics.createEmptyTag = function (tag, callback) {
 		if (!tag) {
 			return callback(new Error('[[error:invalid-tag]]'));
@@ -273,7 +297,7 @@ module.exports = function (Topics) {
 				if (plugins.hasListeners('filter:topics.searchTags')) {
 					plugins.fireHook('filter:topics.searchTags', {data: data}, next);
 				} else {
-					findMatches(data.query, next);
+					findMatches(data.query, 0, next);
 				}
 			},
 			function (result, next) {
@@ -295,7 +319,7 @@ module.exports = function (Topics) {
 				if (plugins.hasListeners('filter:topics.autocompleteTags')) {
 					plugins.fireHook('filter:topics.autocompleteTags', {data: data}, next);
 				} else {
-					findMatches(data.query, next);
+					findMatches(data.query, data.cid, next);
 				}
 			},
 			function (result, next) {
@@ -304,10 +328,21 @@ module.exports = function (Topics) {
 		], callback);
 	};
 
-	function findMatches(query, callback) {
+	function findMatches(query, cid, callback) {
 		async.waterfall([
 			function (next) {
-				db.getSortedSetRevRange('tags:topic:count', 0, -1, next);
+				if (parseInt(cid, 10)) {
+					db.getSortedSetRange('cid:' + cid + ':tag:whitelist', 0, -1, next);
+				} else {
+					setImmediate(next, null, []);
+				}
+			},
+			function (tagWhitelist, next) {
+				if (tagWhitelist.length) {
+					setImmediate(next, null, tagWhitelist);
+				} else {
+					db.getSortedSetRevRange('tags:topic:count', 0, -1, next);
+				}
 			},
 			function (tags, next) {
 				query = query.toLowerCase();
diff --git a/src/views/admin/manage/category.tpl b/src/views/admin/manage/category.tpl
index 814b319913..7b2638b584 100644
--- a/src/views/admin/manage/category.tpl
+++ b/src/views/admin/manage/category.tpl
@@ -59,6 +59,10 @@
 								</div>
 							</div>
 						</fieldset>
+						<fieldset>
+							<label for="tag-whitelist">Tag Whitelist</label><br />
+							<input id="tag-whitelist" type="text" class="form-control" placeholder="Enter category tags here" data-name="tagWhitelist" value="" />
+						</fieldset>
 					</div>
 				</div>
 
diff --git a/test/categories.js b/test/categories.js
index 1a95846264..e0e035410b 100644
--- a/test/categories.js
+++ b/test/categories.js
@@ -541,7 +541,80 @@ describe('Categories', function () {
 				});
 			});
 		});
+	});
+
+	describe('tag whitelist', function () {
+		var cid;
+		var socketTopics = require('../src/socket.io/topics');
+		before(function (done) {
+			Categories.create({
+				name: 'test'
+			}, function (err, category) {
+				assert.ifError(err);
+				cid = category.cid;
+				done();
+			});
+		});
+
+		it('should error if data is invalid', function (done) {
+			socketTopics.isTagAllowed({uid: posterUid}, null, function (err) {
+				assert.equal(err.message, '[[error:invalid-data]]');
+				done();
+			});
+		});
+
+		it('should return true if category whitelist is empty', function (done) {
+			socketTopics.isTagAllowed({uid: posterUid}, {tag: 'notallowed', cid: cid}, function (err, allowed) {
+				assert.ifError(err);
+				assert(allowed);
+				done();
+			});
+		});
 
+		it('should add tags to category whitelist', function (done) {
+			var data = {};
+			data[cid] = {
+				tagWhitelist: 'nodebb,jquery,javascript'
+			};
+			Categories.update(data, function (err) {
+				assert.ifError(err);
+				db.getSortedSetRange('cid:' + cid + ':tag:whitelist', 0, -1, function (err, tagWhitelist) {
+					assert.ifError(err);
+					assert.deepEqual(['nodebb', 'jquery', 'javascript'], tagWhitelist);
+					done();
+				});
+			});
+		});
+
+		it('should return false if category whitelist does not have tag', function (done) {
+			socketTopics.isTagAllowed({uid: posterUid}, {tag: 'notallowed', cid: cid}, function (err, allowed) {
+				assert.ifError(err);
+				assert(!allowed);
+				done();
+			});
+		});
+
+		it('should return true if category whitelist has tag', function (done) {
+			socketTopics.isTagAllowed({uid: posterUid}, {tag: 'nodebb', cid: cid}, function (err, allowed) {
+				assert.ifError(err);
+				assert(allowed);
+				done();
+			});
+		});
+
+		it('should post a topic with only allowed tags', function (done) {
+			Topics.post({
+				uid: posterUid,
+				cid: cid,
+				title: 'Test Topic Title',
+				content: 'The content of test topic',
+				tags: ['nodebb', 'jquery', 'notallowed']
+			}, function (err, data) {
+				assert.ifError(err);
+				assert.equal(data.topicData.tags.length, 2);
+				done();
+			});
+		});
 	});