Merge "Set default TTL for memcache item expiration" into release/2019.2.0
diff --git a/README.rst b/README.rst
index 5a0fedf..6ce524f 100644
--- a/README.rst
+++ b/README.rst
@@ -869,6 +869,111 @@
 You can read more about it here:
     https://docs.openstack.org/security-guide/databases/database-access-control.html
 
+Enable security compliance policies.
+-----------------------------------
+By default security compliance policies disabled. You are able to define follow params independency each other.
+
+Notice: To ignore `change_password_upon_first_use` requirement for specific users, such as service users,
+set the `options` attribute `ignore_change_password_upon_first_use`
+to `True` for the desired user via the update user API
+
+Notice: Symbol "$" should have escape character and looks like "$$".
+
+.. code-block:: yaml
+
+keystone:
+  server:
+    security_compliance:
+      disable_user_account_days_inactive: 90
+      lockout_failure_attempts: 5
+      lockout_duration: 600
+      password_expires_days: 90
+      unique_last_password_count: 10
+      minimum_password_age: 0
+      password_regex: '^(?=.*\d)(?=.*[a-zA-Z]).{7,}$$'
+      password_regex_description: 'Your password must contains at least 1 letter, 1 digit, and have a minimum length of 7 characters'
+      change_password_upon_first_use: true
+
+
+Define extra user options.
+-------------------------
+
+  To ignore `change_password_upon_first_use` requirement for specific users,
+such as service users, set the `options` attribute `ignore_change_password_upon_first_use`
+to `True` for the desired user via the update user API.
+
+  To ignore `password_expires_days` requirement for specific users,
+such as service users, set the `options` attribute `ignore_password_expiry`
+to `True` for the desired user via the update user API.
+
+  To ignore `lockout_failure_attempts` requirement for specific users,
+such as service users, set the `options` attribute `ignore_lockout_failure_attempts`
+to `True` for the desired user via the update user API.
+
+  Also If there exists a user who should not be able to change her own password via
+the keystone password change API, keystone supports setting that user’s option `lock_password`
+to True via the user update API.
+
+#For release since Q
+.. code-block:: yaml
+
+keystone:
+    client:
+      resources:
+        v3:
+          users:
+            cinder:
+              options:
+                ignore_change_password_upon_first_use: True
+                ignore_password_expiry: False
+                ignore_lockout_failure_attempts: False
+                lock_password: True
+.. code-block::
+
+#For all early releases
+.. code-block:: yaml
+
+keystone:
+  client:
+    server:
+      identity:
+        project:
+          service:
+            user:
+              cinder:
+                options:
+                  ignore_change_password_upon_first_use: True
+                  ignore_password_expiry: False
+                  ignore_lockout_failure_attempts: False
+                  lock_password: True
+.. code-block::
+
+Enhanced max_active_keys setup
+------------------------------
+
+Rotating keys too frequently, or with ``[fernet_tokens] max_active_keys`` set too low,
+will cause tokens to become invalid prior to their expiration. As tokens may be fetched
+beyond their initial expiration period, keys should not be fully rotated within the
+period of ``[token] expiration`` + ``[token] allow_expired_window`` seconds to prevent the
+tokens becoming unavailable. As an example, the max_active_keys default value can be
+adjusted according to the following specified values:
+``[token] allow_expired_window`` = 86400 (24 hours)
+``[token] expiration`` = 3600 (1 hour)
+rotation_frequency = 1 (1 hour)
+``[fernet_token]max_active_keys`` = (24 + 1)/1 + 2 = 27
+
+.. code-block:: yaml
+
+    keystone:
+      server:
+        ...
+        tokens:
+          engine: fernet
+          expiration: 3600
+          allow_expired_window: 86400
+          max_active_keys: 27
+        ...
+
 Upgrades
 ========
 
diff --git a/_modules/keystoneng.py b/_modules/keystoneng.py
index 76b6870..ddf0da5 100644
--- a/_modules/keystoneng.py
+++ b/_modules/keystoneng.py
@@ -1082,11 +1082,11 @@
     return ret
 
 
-def user_update(user_id=None, name=None, email=None, enabled=None,
+def user_update(user_id=None, name=None, email=None, enabled=None, options=None,
                 tenant=None, profile=None, project=None, description=None, **connection_args):
     '''
     Update a user's information (keystone user-update)
-    The following fields may be updated: name, email, enabled, tenant.
+    The following fields may be updated: name, email, enabled, tenant, options.
     Because the name is one of the fields, a valid user id is required.
 
     CLI Examples:
@@ -1112,6 +1112,8 @@
         email = user.email
     if enabled is None:
         enabled = user.enabled
+    if not options:
+       options = {}
 
     if _client_version(kstone) > 2:
         if description is None:
@@ -1129,9 +1131,9 @@
             project_id = getattr(user, 'project_id', None)
 
         kstone.users.update(user=user_id, name=name, email=email, enabled=enabled, description=description,
-                            project_id=project_id)
+                            options=options, project_id=project_id)
     else:
-        kstone.users.update(user=user_id, name=name, email=email, enabled=enabled)
+        kstone.users.update(user=user_id, name=name, email=email, options=options, enabled=enabled)
 
         tenant_id = None
         if tenant:
diff --git a/_modules/keystonev3/common.py b/_modules/keystonev3/common.py
index 93cfe6d..9f2c8b1 100644
--- a/_modules/keystonev3/common.py
+++ b/_modules/keystonev3/common.py
@@ -1,5 +1,9 @@
 import logging
 import os_client_config
+import functools
+import time
+
+from keystoneclient import exceptions as ks_exceptions
 
 log = logging.getLogger(__name__)
 
@@ -64,33 +68,60 @@
 
 def send(method, microversion_header=None):
     def wrap(func):
+        @functools.wraps(func)
         def wrapped_f(*args, **kwargs):
             headers = kwargs.pop('headers', {})
             if kwargs.get('microversion'):
                 headers.setdefault(microversion_header,
                                    kwargs.get('microversion'))
             cloud_name = kwargs.pop('cloud_name')
+            connect_retries = 30
+            connect_retry_delay = 1
             if not cloud_name:
                 e = NoCredentials()
                 log.error('%s' % e)
                 raise e
-            adapter = _get_raw_client(cloud_name)
+            adapter = None
+            for i in range(connect_retries):
+              try:
+                adapter = _get_raw_client(cloud_name)
+              except (ks_exceptions.DiscoveryFailure) as e:
+                msg = ("Got exception when determining a suitable "
+                       "URL for Keystone plugin. Sleeping for %ss. Attepmpts "
+                       "%s of %s")
+                log.error(msg % (connect_retry_delay, i, connect_retries))
+                time.sleep(connect_retry_delay)
+                continue
+              break
+            if not adapter:
+              raise ks_exceptions.DiscoveryFailure("Could not connect to Keystone API to determine a suitable URL for the plugin")
             # Remove salt internal kwargs
             kwarg_keys = list(kwargs.keys())
             for k in kwarg_keys:
                 if k.startswith('__'):
                     kwargs.pop(k)
-            url, json = func(*args, **kwargs)
-            if json:
-                response = getattr(adapter, method)(url, headers=headers,
-                                                    json=json)
-            else:
-                response = getattr(adapter, method)(url, headers=headers)
-            if not response.content:
+            url, json  = func(*args, **kwargs)
+            response = None
+            for i in range(connect_retries):
+                try:
+                  response = getattr(adapter, method)(
+                      url, connect_retries=connect_retries,
+                      json=json)
+                except Exception as e:
+                    if not hasattr(e, 'http_status') or (e.http_status >= 500
+                        or e.http_status == 0):
+                        msg = ("Got retriable exception when contacting "
+                               "Keystone API. Sleeping for %ss. Attepmpts "
+                               "%s of %s")
+                        log.error(msg % (connect_retry_delay, i, connect_retries))
+                        time.sleep(connect_retry_delay)
+                        continue
+                break
+            if not response or not response.content:
                 return {}
             try:
                 resp = response.json()
-            except:
+            except ValueError:
                 resp = response.content
             return resp
         return wrapped_f
diff --git a/_states/keystoneng.py b/_states/keystoneng.py
index 82ce494..36a0d52 100644
--- a/_states/keystoneng.py
+++ b/_states/keystoneng.py
@@ -105,6 +105,7 @@
                  profile=None,
                  password_reset=True,
                  project=None,
+                 options=None,
                  **connection_args):
     '''
     Ensure that the keystone user is present with the specified properties.
@@ -138,6 +139,9 @@
     enabled
         Availability state for this user
 
+    options
+        Dictionary of extra user options.
+
     roles
         The roles the user should have under given tenants.
         Passed as a dictionary mapping tenant names to a list
@@ -182,6 +186,7 @@
         change_enabled = False
         change_tenant = False
         change_password = False
+        change_options = False
 
         if user[name].get('email', None) != email:
             change_email = True
@@ -200,7 +205,15 @@
                                                           **connection_args)):
             change_password = True
 
-        if __opts__.get('test') and (change_email or change_enabled or change_tenant or change_password):
+        if options:
+            options_to_update = {option: options[option] for option in options
+                                 if (option not in user[name]['options'])
+                                 or (options[option] != user[name]['options'][option])}
+
+            if len(options_to_update):
+                change_options = True
+
+        if __opts__.get('test') and (change_email or change_enabled or change_tenant or change_password or change_options):
             ret['result'] = None
             ret['comment'] = 'User "{0}" will be updated'.format(name)
             if change_email is True:
@@ -211,6 +224,8 @@
                 ret['changes']['Tenant'] = 'Will be added to "{0}" tenant'.format(tenant)
             if change_password is True:
                 ret['changes']['Password'] = 'Will be updated'
+            if change_options is True:
+                ret['changes']['Options'] = 'Will be updated'
             return ret
 
         ret['comment'] = 'User "{0}" is already present'.format(name)
@@ -236,6 +251,11 @@
             ret['comment'] = 'User "{0}" has been updated'.format(name)
             ret['changes']['Password'] = 'Updated'
 
+        if change_options:
+            __salt__['keystoneng.user_update'](name=name, options=options_to_update, profile=profile, **connection_args)
+            ret['comment'] = 'Options has been updated'
+            ret['changes']['Options'] = options_to_update
+
         if roles:
             for tenant in roles:
                 args = dict({'user_name': name, 'tenant_name':
@@ -283,6 +303,7 @@
                                          email=email,
                                          tenant_id=tenant_id,
                                          enabled=enabled,
+                                         options=options,
                                          profile=profile,
                                          **connection_args)
         if roles:
diff --git a/_states/keystonev3.py b/_states/keystonev3.py
index 2dd651d..9f287d5 100644
--- a/_states/keystonev3.py
+++ b/_states/keystonev3.py
@@ -251,15 +251,23 @@
         exact_user = users[0]
         user_id = exact_user['id']
         changable = (
-            'default_project_id', 'domain_id', 'enabled', 'email'
+            'default_project_id', 'domain_id', 'enabled', 'email', 'options'
         )
         if password_reset:
             changable += ('password',)
         to_update = {}
 
         for key in kwargs:
-            if (key in changable and (key not in exact_user or
-                                      kwargs[key] != exact_user[key])):
+            if key in changable:
+                if key == 'options':
+                    to_update['options'] = {option: value for option, value in kwargs['options'].items()
+                                            if (option not in exact_user['options'])
+                                            or (value != exact_user['options'][option])}
+
+                    if not len(to_update['options']):
+                        del to_update['options']
+
+                elif key not in exact_user or kwargs[key] != exact_user[key]:
                     to_update[key] = kwargs[key]
 
         if to_update:
diff --git a/keystone/client/project.sls b/keystone/client/project.sls
index 240c99f..c8179d9 100644
--- a/keystone/client/project.sls
+++ b/keystone/client/project.sls
@@ -33,6 +33,21 @@
   - password: {{ user.password }}
   - email: {{ user.get('email', 'root@localhost') }}
   - tenant: {{ tenant_name }}
+  {%- if user.options is defined %}
+  - options:
+    {%- if user.options.lock_password is defined %}
+      lock_password: {{ user.options.lock_password }}
+    {%- endif %}
+    {%- if user.options.ignore_change_password_upon_first_use is defined %}
+      ignore_change_password_upon_first_use: {{ user.options.ignore_change_password_upon_first_use }}
+    {%- endif %}
+    {%- if user.options.ignore_password_expiry is defined %}
+      ignore_password_expiry: {{ user.options.ignore_password_expiry }}
+    {%- endif %}
+    {%- if user.options.ignore_lockout_failure_attempts is defined %}
+      ignore_lockout_failure_attempts: {{ user.options.ignore_lockout_failure_attempts }}
+    {%- endif %}
+  {%- endif %}
   - roles:
       "{{ tenant_name }}":
         {%- if user.get('is_admin', False) %}
diff --git a/keystone/client/resources/v3.sls b/keystone/client/resources/v3.sls
index 7803233..8f58f0c 100644
--- a/keystone/client/resources/v3.sls
+++ b/keystone/client/resources/v3.sls
@@ -172,6 +172,21 @@
     {%- if user.password_reset is defined %}
     - password_reset: {{ user.password_reset }}
     {%- endif %}
+    {%- if user.options is defined %}
+    - options:
+      {%- if user.options.lock_password is defined %}
+        lock_password: {{ user.options.lock_password }}
+      {%- endif %}
+      {%- if user.options.ignore_change_password_upon_first_use is defined %}
+        ignore_change_password_upon_first_use: {{ user.options.ignore_change_password_upon_first_use }}
+      {%- endif %}
+      {%- if user.options.ignore_password_expiry is defined %}
+        ignore_password_expiry: {{ user.options.ignore_password_expiry }}
+      {%- endif %}
+      {%- if user.options.ignore_lockout_failure_attempts is defined %}
+        ignore_lockout_failure_attempts: {{ user.options.ignore_lockout_failure_attempts }}
+      {%- endif %}
+    {%- endif %}
 
   {%- elif user.get('status', 'present') == 'absent' %}
 
diff --git a/keystone/client/server.sls b/keystone/client/server.sls
index 9f97b74..bf83eeb 100644
--- a/keystone/client/server.sls
+++ b/keystone/client/server.sls
@@ -149,6 +149,21 @@
   - email: {{ user.email }}
   {%- endif %}
   - tenant: {{ tenant_name }}
+  {%- if user.options is defined %}
+  - options:
+    {%- if user.options.lock_password is defined %}
+      lock_password: {{ user.options.lock_password }}
+    {%- endif %}
+    {%- if user.options.ignore_change_password_upon_first_use is defined %}
+      ignore_change_password_upon_first_use: {{ user.options.ignore_change_password_upon_first_use }}
+    {%- endif %}
+    {%- if user.options.ignore_password_expiry is defined %}
+      ignore_password_expiry: {{ user.options.ignore_password_expiry }}
+    {%- endif %}
+    {%- if user.options.ignore_lockout_failure_attempts is defined %}
+      ignore_lockout_failure_attempts: {{ user.options.ignore_lockout_failure_attempts }}
+    {%- endif %}
+  {%- endif %}
   - roles:
       "{{ tenant_name }}":
         {%- if user.get('is_admin', False) %}
diff --git a/keystone/files/newton/keystone.conf.Debian b/keystone/files/newton/keystone.conf.Debian
index 8d22cf4..e176b99 100644
--- a/keystone/files/newton/keystone.conf.Debian
+++ b/keystone/files/newton/keystone.conf.Debian
@@ -2594,6 +2594,9 @@
 # (integer value)
 # Minimum value: 1
 #disable_user_account_days_inactive = <None>
+{%- if server.security_compliance.disable_user_account_days_inactive is defined and server.get('backend', 'sql') == 'sql' %}
+disable_user_account_days_inactive = {{ server.security_compliance.disable_user_account_days_inactive }}
+{%- endif %}
 
 # The maximum number of times that a user can fail to authenticate before the
 # user account is locked for the number of seconds specified by
@@ -2604,6 +2607,9 @@
 # backend for the `[identity] driver`. (integer value)
 # Minimum value: 1
 #lockout_failure_attempts = <None>
+{%- if server.security_compliance.lockout_failure_attempts is defined and server.get('backend', 'sql') == 'sql' %}
+lockout_failure_attempts = {{ server.security_compliance.lockout_failure_attempts }}
+{%- endif %}
 
 # The number of seconds a user account will be locked when the maximum number
 # of failed authentication attempts (as specified by `[security_compliance]
@@ -2613,6 +2619,9 @@
 # `[identity] driver`. (integer value)
 # Minimum value: 1
 #lockout_duration = 1800
+{%- if server.security_compliance.lockout_duration is defined and server.get('backend', 'sql') == 'sql' %}
+lockout_duration = {{ server.security_compliance.lockout_duration }}
+{%- endif %}
 
 # The number of days for which a password will be considered valid before
 # requiring it to be changed. This feature is disabled by default. If enabled,
@@ -2621,12 +2630,18 @@
 # `[identity] driver`. (integer value)
 # Minimum value: 1
 #password_expires_days = <None>
+{%- if server.security_compliance.password_expires_days is defined and server.get('backend', 'sql') == 'sql' %}
+password_expires_days = {{ server.security_compliance.password_expires_days }}
+{%- endif %}
 
 # Comma separated list of user IDs to be ignored when checking if a password is
 # expired. Passwords for users in this list will not expire. This feature will
 # only be enabled if `[security_compliance] password_expires_days` is set.
 # (list value)
 #password_expires_ignore_user_ids =
+{%- if server.password_expires_ignore_user_ids is defined and server.password_expires_days is defined and server.get('backend', 'sql') == 'sql' %}
+password_expires_ignore_user_ids = {{ server.password_expires_ignore_user_ids }}
+{%- endif %}
 
 # This controls the number of previous user password iterations to keep in
 # history, in order to enforce that newly created passwords are unique. Setting
@@ -2635,6 +2650,9 @@
 # backend for the `[identity] driver`. (integer value)
 # Minimum value: 1
 #unique_last_password_count = 1
+{%- if server.security_compliance.unique_last_password_count is defined and server.get('backend', 'sql') == 'sql' %}
+unique_last_password_count = {{ server.security_compliance.unique_last_password_count }}
+{%- endif %}
 
 # The number of days that a password must be used before the user can change
 # it. This prevents users from changing their passwords immediately in order to
@@ -2646,6 +2664,9 @@
 # option should be less than the `password_expires_days`. (integer value)
 # Minimum value: 0
 #minimum_password_age = 0
+{%- if server.security_compliance.minimum_password_age is defined and server.get('backend', 'sql') == 'sql' %}
+minimum_password_age = {{ server.security_compliance.minimum_password_age }}
+{%- endif %}
 
 # The regular expression used to validate password strength requirements. By
 # default, the regular expression will match any password. The following is an
@@ -2653,13 +2674,18 @@
 # minimum length of 7 characters: ^(?=.*\d)(?=.*[a-zA-Z]).{7,}$ This feature
 # depends on the `sql` backend for the `[identity] driver`. (string value)
 #password_regex = <None>
+{%- if server.security_compliance.password_regex is defined and server.get('backend', 'sql') == 'sql' %}
+password_regex = {{ server.security_compliance.password_regex }}
+{%- endif %}
 
 # Describe your password regular expression here in language for humans. If a
 # password fails to match the regular expression, the contents of this
 # configuration variable will be returned to users to explain why their
 # requested password was insufficient. (string value)
 #password_regex_description = <None>
-
+{%- if server.security_compliance.password_regex_description is defined %}
+password_regex_description = {{ server.security_compliance.password_regex_description }}
+{%- endif %}
 
 [shadow_users]
 
diff --git a/keystone/files/ocata/keystone.conf.Debian b/keystone/files/ocata/keystone.conf.Debian
index a2c50eb..d7372f9 100644
--- a/keystone/files/ocata/keystone.conf.Debian
+++ b/keystone/files/ocata/keystone.conf.Debian
@@ -2805,6 +2805,9 @@
 # (integer value)
 # Minimum value: 1
 #disable_user_account_days_inactive = <None>
+{%- if server.security_compliance.disable_user_account_days_inactive is defined and server.get('backend', 'sql') == 'sql' %}
+disable_user_account_days_inactive = {{ server.security_compliance.disable_user_account_days_inactive }}
+{%- endif %}
 
 # The maximum number of times that a user can fail to authenticate before the
 # user account is locked for the number of seconds specified by
@@ -2815,6 +2818,9 @@
 # backend for the `[identity] driver`. (integer value)
 # Minimum value: 1
 #lockout_failure_attempts = <None>
+{%- if server.security_compliance.lockout_failure_attempts is defined and server.get('backend', 'sql') == 'sql' %}
+lockout_failure_attempts = {{ server.security_compliance.lockout_failure_attempts }}
+{%- endif %}
 
 # The number of seconds a user account will be locked when the maximum number
 # of failed authentication attempts (as specified by `[security_compliance]
@@ -2824,6 +2830,9 @@
 # `[identity] driver`. (integer value)
 # Minimum value: 1
 #lockout_duration = 1800
+{%- if server.security_compliance.lockout_duration is defined and server.get('backend', 'sql') == 'sql' %}
+lockout_duration = {{ server.security_compliance.lockout_duration }}
+{%- endif %}
 
 # The number of days for which a password will be considered valid before
 # requiring it to be changed. This feature is disabled by default. If enabled,
@@ -2832,20 +2841,9 @@
 # `[identity] driver`. (integer value)
 # Minimum value: 1
 #password_expires_days = <None>
-
-# DEPRECATED: Comma separated list of user IDs to be ignored when checking if a
-# password is expired. Passwords for users in this list will not expire. This
-# feature will only be enabled if `[security_compliance] password_expires_days`
-# is set. (list value)
-# This option is deprecated for removal since O.
-# Its value may be silently ignored in the future.
-# Reason: Functionality added as a per-user option "ignore_password_expiry" in
-# Ocata. Each user that should ignore password expiry should have the value set
-# to "true" in the user's `options` attribute (e.g.
-# `user['options']['ignore_password_expiry'] = True`) with an "update_user"
-# call. This avoids the need to restart keystone to adjust the users that
-# ignore password expiry. This option will be removed in the Pike release.
-#password_expires_ignore_user_ids =
+{%- if server.security_compliance.password_expires_days is defined and server.get('backend', 'sql') == 'sql' %}
+password_expires_days = {{ server.security_compliance.password_expires_days }}
+{%- endif %}
 
 # This controls the number of previous user password iterations to keep in
 # history, in order to enforce that newly created passwords are unique. Setting
@@ -2854,6 +2852,9 @@
 # backend for the `[identity] driver`. (integer value)
 # Minimum value: 1
 #unique_last_password_count = 1
+{%- if server.security_compliance.unique_last_password_count is defined and server.get('backend', 'sql') == 'sql' %}
+unique_last_password_count = {{ server.security_compliance.unique_last_password_count }}
+{%- endif %}
 
 # The number of days that a password must be used before the user can change
 # it. This prevents users from changing their passwords immediately in order to
@@ -2865,6 +2866,9 @@
 # option should be less than the `password_expires_days`. (integer value)
 # Minimum value: 0
 #minimum_password_age = 0
+{%- if server.security_compliance.minimum_password_age is defined and server.get('backend', 'sql') == 'sql' %}
+minimum_password_age = {{ server.security_compliance.minimum_password_age }}
+{%- endif %}
 
 # The regular expression used to validate password strength requirements. By
 # default, the regular expression will match any password. The following is an
@@ -2872,12 +2876,18 @@
 # minimum length of 7 characters: ^(?=.*\d)(?=.*[a-zA-Z]).{7,}$ This feature
 # depends on the `sql` backend for the `[identity] driver`. (string value)
 #password_regex = <None>
+{%- if server.security_compliance.password_regex is defined and server.get('backend', 'sql') == 'sql' %}
+password_regex = {{ server.security_compliance.password_regex }}
+{%- endif %}
 
 # Describe your password regular expression here in language for humans. If a
 # password fails to match the regular expression, the contents of this
 # configuration variable will be returned to users to explain why their
 # requested password was insufficient. (string value)
 #password_regex_description = <None>
+{%- if server.security_compliance.password_regex_description is defined %}
+password_regex_description = {{ server.security_compliance.password_regex_description }}
+{%- endif %}
 
 # Enabling this option requires users to change their password when the user is
 # created, or upon administrative reset. Before accessing any services,
@@ -2888,7 +2898,9 @@
 # only applicable with the `sql` backend for the `[identity] driver`. (boolean
 # value)
 #change_password_upon_first_use = false
-
+{%- if server.security_compliance.change_password_upon_first_use is defined and server.get('backend', 'sql') == 'sql' %}
+change_password_upon_first_use = {{ server.security_compliance.change_password_upon_first_use }}
+{%- endif %}
 
 [shadow_users]
 
@@ -3060,6 +3072,9 @@
 # the built-in expiry time. This allows long running operations to succeed.
 # Defaults to two days. (integer value)
 #allow_expired_window = 172800
+{%- if server.tokens.allow_expired_window is defined %}
+allow_expired_window = {{ server.tokens.allow_expired_window }}
+{%- endif %}
 
 hash_algorithm = {{ server.hash_algorithm }}
 
diff --git a/keystone/files/pike/keystone.conf.Debian b/keystone/files/pike/keystone.conf.Debian
index 4f3ef6d..ba8c9f9 100644
--- a/keystone/files/pike/keystone.conf.Debian
+++ b/keystone/files/pike/keystone.conf.Debian
@@ -2822,6 +2822,9 @@
 # (integer value)
 # Minimum value: 1
 #disable_user_account_days_inactive = <None>
+{%- if server.security_compliance.disable_user_account_days_inactive is defined and server.get('backend', 'sql') == 'sql' %}
+disable_user_account_days_inactive = {{ server.security_compliance.disable_user_account_days_inactive }}
+{%- endif %}
 
 # The maximum number of times that a user can fail to authenticate before the
 # user account is locked for the number of seconds specified by
@@ -2832,6 +2835,9 @@
 # backend for the `[identity] driver`. (integer value)
 # Minimum value: 1
 #lockout_failure_attempts = <None>
+{%- if server.security_compliance.lockout_failure_attempts is defined and server.get('backend', 'sql') == 'sql' %}
+lockout_failure_attempts = {{ server.security_compliance.lockout_failure_attempts }}
+{%- endif %}
 
 # The number of seconds a user account will be locked when the maximum number
 # of failed authentication attempts (as specified by `[security_compliance]
@@ -2841,6 +2847,9 @@
 # `[identity] driver`. (integer value)
 # Minimum value: 1
 #lockout_duration = 1800
+{%- if server.security_compliance.lockout_duration is defined and server.get('backend', 'sql') == 'sql' %}
+lockout_duration = {{ server.security_compliance.lockout_duration }}
+{%- endif %}
 
 # The number of days for which a password will be considered valid before
 # requiring it to be changed. This feature is disabled by default. If enabled,
@@ -2849,20 +2858,9 @@
 # `[identity] driver`. (integer value)
 # Minimum value: 1
 #password_expires_days = <None>
-
-# DEPRECATED: Comma separated list of user IDs to be ignored when checking if a
-# password is expired. Passwords for users in this list will not expire. This
-# feature will only be enabled if `[security_compliance] password_expires_days`
-# is set. (list value)
-# This option is deprecated for removal since O.
-# Its value may be silently ignored in the future.
-# Reason: Functionality added as a per-user option "ignore_password_expiry" in
-# Ocata. Each user that should ignore password expiry should have the value set
-# to "true" in the user's `options` attribute (e.g.
-# `user['options']['ignore_password_expiry'] = True`) with an "update_user"
-# call. This avoids the need to restart keystone to adjust the users that
-# ignore password expiry. This option will be removed in the Pike release.
-#password_expires_ignore_user_ids =
+{%- if server.security_compliance.password_expires_days is defined and server.get('backend', 'sql') == 'sql' %}
+password_expires_days = {{ server.security_compliance.password_expires_days }}
+{%- endif %}
 
 # This controls the number of previous user password iterations to keep in
 # history, in order to enforce that newly created passwords are unique. Setting
@@ -2871,6 +2869,9 @@
 # backend for the `[identity] driver`. (integer value)
 # Minimum value: 1
 #unique_last_password_count = 1
+{%- if server.security_compliance.unique_last_password_count is defined and server.get('backend', 'sql') == 'sql' %}
+unique_last_password_count = {{ server.security_compliance.unique_last_password_count }}
+{%- endif %}
 
 # The number of days that a password must be used before the user can change
 # it. This prevents users from changing their passwords immediately in order to
@@ -2882,6 +2883,9 @@
 # option should be less than the `password_expires_days`. (integer value)
 # Minimum value: 0
 #minimum_password_age = 0
+{%- if server.security_compliance.minimum_password_age is defined and server.get('backend', 'sql') == 'sql' %}
+minimum_password_age = {{ server.security_compliance.minimum_password_age }}
+{%- endif %}
 
 # The regular expression used to validate password strength requirements. By
 # default, the regular expression will match any password. The following is an
@@ -2889,12 +2893,18 @@
 # minimum length of 7 characters: ^(?=.*\d)(?=.*[a-zA-Z]).{7,}$ This feature
 # depends on the `sql` backend for the `[identity] driver`. (string value)
 #password_regex = <None>
+{%- if server.security_compliance.password_regex is defined and server.get('backend', 'sql') == 'sql' %}
+password_regex = {{ server.security_compliance.password_regex }}
+{%- endif %}
 
 # Describe your password regular expression here in language for humans. If a
 # password fails to match the regular expression, the contents of this
 # configuration variable will be returned to users to explain why their
 # requested password was insufficient. (string value)
 #password_regex_description = <None>
+{%- if server.security_compliance.password_regex_description is defined %}
+password_regex_description = {{ server.security_compliance.password_regex_description }}
+{%- endif %}
 
 # Enabling this option requires users to change their password when the user is
 # created, or upon administrative reset. Before accessing any services,
@@ -2905,7 +2915,9 @@
 # only applicable with the `sql` backend for the `[identity] driver`. (boolean
 # value)
 #change_password_upon_first_use = false
-
+{%- if server.security_compliance.change_password_upon_first_use is defined and server.get('backend', 'sql') == 'sql' %}
+change_password_upon_first_use = {{ server.security_compliance.change_password_upon_first_use }}
+{%- endif %}
 
 [shadow_users]
 
@@ -3078,6 +3090,9 @@
 # the built-in expiry time. This allows long running operations to succeed.
 # Defaults to two days. (integer value)
 #allow_expired_window = 172800
+{%- if server.tokens.allow_expired_window is defined %}
+allow_expired_window = {{ server.tokens.allow_expired_window }}
+{%- endif %}
 
 hash_algorithm = {{ server.hash_algorithm }}
 
diff --git a/keystone/files/queens/keystone.conf.Debian b/keystone/files/queens/keystone.conf.Debian
index c4d0c1b..f872c2f 100644
--- a/keystone/files/queens/keystone.conf.Debian
+++ b/keystone/files/queens/keystone.conf.Debian
@@ -1399,6 +1399,9 @@
 # user table. (integer value)
 # Minimum value: 1
 #disable_user_account_days_inactive = <None>
+{%- if server.security_compliance.disable_user_account_days_inactive is defined and server.get('backend', 'sql') == 'sql' %}
+disable_user_account_days_inactive = {{ server.security_compliance.disable_user_account_days_inactive }}
+{%- endif %}
 
 # The maximum number of times that a user can fail to authenticate
 # before the user account is locked for the number of seconds
@@ -1410,6 +1413,9 @@
 # `[identity] driver`. (integer value)
 # Minimum value: 1
 #lockout_failure_attempts = <None>
+{%- if server.security_compliance.lockout_failure_attempts is defined and server.get('backend', 'sql') == 'sql' %}
+lockout_failure_attempts = {{ server.security_compliance.lockout_failure_attempts }}
+{%- endif %}
 
 # The number of seconds a user account will be locked when the maximum
 # number of failed authentication attempts (as specified by
@@ -1420,6 +1426,9 @@
 # driver`. (integer value)
 # Minimum value: 1
 #lockout_duration = 1800
+{%- if server.security_compliance.lockout_duration is defined and server.get('backend', 'sql') == 'sql' %}
+lockout_duration = {{ server.security_compliance.lockout_duration }}
+{%- endif %}
 
 # The number of days for which a password will be considered valid
 # before requiring it to be changed. This feature is disabled by
@@ -1429,6 +1438,9 @@
 # value)
 # Minimum value: 1
 #password_expires_days = <None>
+{%- if server.security_compliance.password_expires_days is defined and server.get('backend', 'sql') == 'sql' %}
+password_expires_days = {{ server.security_compliance.password_expires_days }}
+{%- endif %}
 
 # This controls the number of previous user password iterations to
 # keep in history, in order to enforce that newly created passwords
@@ -1439,6 +1451,9 @@
 # for the `[identity] driver`. (integer value)
 # Minimum value: 1
 #unique_last_password_count = 1
+{%- if server.security_compliance.unique_last_password_count is defined and server.get('backend', 'sql') == 'sql' %}
+unique_last_password_count = {{ server.security_compliance.unique_last_password_count }}
+{%- endif %}
 
 # The number of days that a password must be used before the user can
 # change it. This prevents users from changing their passwords
@@ -1451,6 +1466,9 @@
 # be less than the `password_expires_days`. (integer value)
 # Minimum value: 0
 #minimum_password_age = 0
+{%- if server.security_compliance.minimum_password_age is defined and server.get('backend', 'sql') == 'sql' %}
+minimum_password_age = {{ server.security_compliance.minimum_password_age }}
+{%- endif %}
 
 # The regular expression used to validate password strength
 # requirements. By default, the regular expression will match any
@@ -1459,6 +1477,9 @@
 # ^(?=.*\d)(?=.*[a-zA-Z]).{7,}$ This feature depends on the `sql`
 # backend for the `[identity] driver`. (string value)
 #password_regex = <None>
+{%- if server.security_compliance.password_regex is defined and server.get('backend', 'sql') == 'sql' %}
+password_regex = {{ server.security_compliance.password_regex }}
+{%- endif %}
 
 # Describe your password regular expression here in language for
 # humans. If a password fails to match the regular expression, the
@@ -1466,6 +1487,9 @@
 # explain why their requested password was insufficient. (string
 # value)
 #password_regex_description = <None>
+{%- if server.security_compliance.password_regex_description is defined %}
+password_regex_description = {{ server.security_compliance.password_regex_description }}
+{%- endif %}
 
 # Enabling this option requires users to change their password when
 # the user is created, or upon administrative reset. Before accessing
@@ -1476,7 +1500,9 @@
 # is disabled by default. This feature is only applicable with the
 # `sql` backend for the `[identity] driver`. (boolean value)
 #change_password_upon_first_use = false
-
+{%- if server.security_compliance.change_password_upon_first_use is defined and server.get('backend', 'sql') == 'sql' %}
+change_password_upon_first_use = {{ server.security_compliance.change_password_upon_first_use }}
+{%- endif %}
 
 [shadow_users]
 
@@ -1687,6 +1713,9 @@
 # for beyond the built-in expiry time. This allows long running
 # operations to succeed. Defaults to two days. (integer value)
 #allow_expired_window = 172800
+{%- if server.tokens.allow_expired_window is defined %}
+allow_expired_window = {{ server.tokens.allow_expired_window }}
+{%- endif %}
 
 hash_algorithm = {{ server.hash_algorithm }}
 
diff --git a/keystone/map.jinja b/keystone/map.jinja
index d97e1a5..cbf1f03 100644
--- a/keystone/map.jinja
+++ b/keystone/map.jinja
@@ -31,6 +31,7 @@
         'notification': False,
         'roles': ['admin', 'Member'],
         'cacert': '/etc/ssl/certs/ca-certificates.crt',
+        'security_compliance': {},
         'logging': {
           'app_name': 'keystone',
           'log_appender': false,
@@ -56,6 +57,7 @@
         'notification': False,
         'roles': ['admin', 'Member'],
         'cacert': '/etc/pki/tls/certs/ca-bundle.crt',
+        'security_compliance': {},
         'logging': {
           'app_name': 'keystone',
           'log_appender': false,
diff --git a/tests/pillar/cluster.sls b/tests/pillar/cluster.sls
index 9d76228..24b17f4 100644
--- a/tests/pillar/cluster.sls
+++ b/tests/pillar/cluster.sls
@@ -24,9 +24,20 @@
     tokens:
       engine: cache
       expiration: 86400
+      allow_expired_window: 86400
       location: /etc/keystone/fernet-keys/
     notification: false
     notification_format: cadf
+    security_compliance:
+      disable_user_account_days_inactive: 90
+      lockout_failure_attempts: 5
+      lockout_duration: 600
+      password_expires_days: 90
+      unique_last_password_count: 10
+      minimum_password_age: 0
+      password_regex: '^(?=.*\d)(?=.*[a-zA-Z]).{7,}$$'
+      password_regex_description: 'Your password must contains at least 1 letter, 1 digit, and have a minimum length of 7 characters'
+      change_password_upon_first_use: True
     logging:
       log_appender: false
       log_handlers:
diff --git a/tests/pillar/single.sls b/tests/pillar/single.sls
index d52812d..3570ed1 100644
--- a/tests/pillar/single.sls
+++ b/tests/pillar/single.sls
@@ -26,8 +26,19 @@
       engine: cache
       expiration: 86400
       location: /etc/keystone/fernet-keys/
+      allow_expired_window: 86400
     notification: false
     notification_format: cadf
+    security_compliance:
+      disable_user_account_days_inactive: 90
+      lockout_failure_attempts: 5
+      lockout_duration: 600
+      password_expires_days: 90
+      unique_last_password_count: 10
+      minimum_password_age: 0
+      password_regex: '^(?=.*\d)(?=.*[a-zA-Z]).{7,}$$'
+      password_regex_description: 'Your password must contains at least 1 letter, 1 digit, and have a minimum length of 7 characters'
+      change_password_upon_first_use: True
     logging:
       log_appender: false
       log_handlers: