diff --git a/examples/single-cluster/templates/hello-world-secret.yaml.j2 b/examples/single-cluster/templates/hello-world-secret.yaml.j2
index c00a3f7..7fd1c5f 100644
--- a/examples/single-cluster/templates/hello-world-secret.yaml.j2
+++ b/examples/single-cluster/templates/hello-world-secret.yaml.j2
@@ -7,4 +7,4 @@ metadata:
     app.kubernetes.io/name: "{{ name }}"
 type: Opaque
 data:
-  PASSWORD: "{{ get_secret('/application') | b64encode }}"
+  PASSWORD: "{{ get_secret('/application', 24) | b64encode }}"
diff --git a/k8t/cli.py b/k8t/cli.py
index b4429f3..78b5a6a 100644
--- a/k8t/cli.py
+++ b/k8t/cli.py
@@ -41,7 +41,7 @@ def root(debug, trace):
 def print_license():
     print(k8t.__license__)
 
-
+# pylint: disable=too-many-locals,too-many-arguments
 @root.command(name="validate", help="Validate template files for given context.")
 @click.option("-m", "--method", type=click.Choice(MERGE_METHODS), default="ltr", show_default=True, help="Value file merge method.")
 @click.option("--value-file", "value_files", multiple=True, type=click.Path(dir_okay=False, exists=True), help="Additional value file to include.")
@@ -49,7 +49,7 @@ def print_license():
 @click.option("--cluster", "-c", "cname", help="Cluster context to use.")
 @click.option("--environment", "-e", "ename", help="Deployment environment to use.")
 @click.argument("directory", type=click.Path(dir_okay=True, file_okay=False, exists=True), default=os.getcwd())
-def cli_validate(method, value_files, cli_values, cname, ename, directory):   # pylint: disable=too-many-locals
+def cli_validate(method, value_files, cli_values, cname, ename, directory):
     if not project.check_directory(directory):
         sys.exit("not a valid project: {}".format(directory))
 
@@ -60,7 +60,7 @@ def cli_validate(method, value_files, cli_values, cname, ename, directory):   #
         envvalues(),
         method=method,
     )
-    conf = config.load_all(directory, cname, ename, method)
+    config.CONFIG = config.load_all(directory, cname, ename, method)
 
     eng = build(directory, cname, ename)
 
@@ -81,7 +81,7 @@ def cli_validate(method, value_files, cli_values, cname, ename, directory):   #
                 errors.add("invalid variable: {}".format(var))
 
         if secrets:
-            if "secrets" not in conf or "provider" not in conf["secrets"]:
+            if "secrets" not in config.CONFIG or "provider" not in config.CONFIG["secrets"]:
                 errors.add("No secrets provider configured")
 
         if errors:
@@ -108,7 +108,6 @@ def cli_gen(method, value_files, cli_values, cname, ename, directory):  # pylint
     if not project.check_directory(directory):
         sys.exit("not a valid project: {}".format(directory))
 
-    conf = config.load_all(directory, cname, ename, method)
     vals = deep_merge(  # pylint: disable=redefined-outer-name
         values.load_all(directory, cname, ename, method),
         *(load_yaml(p) for p in value_files),
@@ -116,6 +115,7 @@ def cli_gen(method, value_files, cli_values, cname, ename, directory):  # pylint
         envvalues(),
         method=method,
     )
+    config.CONFIG = config.load_all(directory, cname, ename, method)
 
     eng = build(directory, cname, ename)
 
@@ -123,7 +123,7 @@ def cli_gen(method, value_files, cli_values, cname, ename, directory):  # pylint
     validated = True
 
     for template_path in templates:
-        if not validate(template_path, vals, eng, conf):
+        if not validate(template_path, vals, eng):
             print("Failed to validate template {}".format(template_path))
 
             validated = False
diff --git a/k8t/config.py b/k8t/config.py
index 9200e42..5c7572d 100644
--- a/k8t/config.py
+++ b/k8t/config.py
@@ -14,13 +14,7 @@ from k8t.project import find_files
 from k8t.util import deep_merge, load_yaml
 
 LOGGER = logging.getLogger(__name__)
-
-
-def validate(config: Dict[str, Any]) -> bool:
-    if "secrets" in config:
-        assert "provider" in config["secrets"]
-
-    return True
+CONFIG = {}
 
 
 def load_all(root: str, cluster: str, environment: str, method: str) -> Dict[str, Any]:
diff --git a/k8t/engine.py b/k8t/engine.py
index b624b30..d94008a 100644
--- a/k8t/engine.py
+++ b/k8t/engine.py
@@ -11,9 +11,9 @@ import logging
 
 from jinja2 import Environment, FileSystemLoader, StrictUndefined
 
-from k8t.filters import b64decode, b64encode, envvar, hashf, random_password
+from k8t.filters import (b64decode, b64encode, envvar, get_secret, hashf,
+                         random_password)
 from k8t.project import find_files
-from k8t.secrets import get_secret
 
 LOGGER = logging.getLogger(__name__)
 
@@ -26,16 +26,16 @@ def build(path: str, cluster: str, environment: str):
 
     env = Environment(undefined=StrictUndefined, loader=FileSystemLoader(template_paths))
 
+    ### Filter functions ###
     env.filters["b64decode"] = b64decode
     env.filters["b64encode"] = b64encode
     env.filters["hash"] = hashf
 
+    ### Global functions ###
     # env.globals['include_raw'] = include_file
     # env.globals['include_file'] = include_file
     env.globals["random_password"] = random_password
-    env.globals["get_secret"] = lambda key: get_secret(
-        key, path, cluster, environment)
-
+    env.globals["get_secret"] = get_secret
     env.globals["env"] = envvar
 
     return env
diff --git a/k8t/environment.py b/k8t/environment.py
index 010ae9a..6f83992 100644
--- a/k8t/environment.py
+++ b/k8t/environment.py
@@ -8,9 +8,8 @@
 # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 
 import os
-from typing import List, Set
+from typing import Set
 
-from k8t.project import find_files
 from k8t.util import list_files, makedirs, touch
 
 
@@ -20,12 +19,12 @@ def list_all(path: str) -> Set[str]:
     result = set()
 
     if os.path.isdir(env_dir):
-        result.update(list_files(env_dir, directories=True))
+        result.update(list_files(env_dir, include_directories=True))
 
     cluster_dir = os.path.join(path, 'clusters')
 
     if os.path.isdir(cluster_dir):
-        for cluster in list_files(cluster_dir, directories=True):
+        for cluster in list_files(cluster_dir, include_directories=True):
             result.update(list_all(os.path.join(cluster_dir, cluster)))
 
     return result
diff --git a/k8t/filters.py b/k8t/filters.py
index 11c8322..c1d3058 100644
--- a/k8t/filters.py
+++ b/k8t/filters.py
@@ -17,6 +17,8 @@ import os
 import string
 from typing import Any
 
+from k8t import config, secret_providers
+
 try:
     from secrets import choice
 except ImportError:
@@ -78,5 +80,15 @@ def hashf(value, method="sha256"):
     return hash_method.hexdigest()
 
 
+def get_secret(key: str, length: int = None) -> str:
+    try:
+        provider = getattr(secret_providers, config.CONFIG["secrets"]["provider"].lower())
 
-# vim: fenc=utf-8:ts=4:sw=4:expandtab
+        return provider(
+            "{0}/{1}".format(config.CONFIG['secrets']['prefix'], key) if "prefix" in config.CONFIG["secrets"] else key,
+            length
+        )
+    except AttributeError:
+        raise NotImplementedError("secret provider {} does not exist.".format(config.CONFIG["secrets"]["provider"].lower()))
+    except KeyError:
+        raise RuntimeError("Secrets provider not configured.")
diff --git a/k8t/project.py b/k8t/project.py
index da54a0b..31ed752 100644
--- a/k8t/project.py
+++ b/k8t/project.py
@@ -10,8 +10,6 @@
 import os
 from typing import List
 
-from simple_tools.interaction import confirm
-
 from k8t.util import makedirs, touch
 
 
@@ -46,6 +44,7 @@ def new(directory: str):
     touch(os.path.join(directory, ".k8t"))
 
 
+# pylint: disable=too-many-arguments
 def find_files(root: str, cluster: str, environment: str, name: str, file_ok=True, dir_ok=True) -> List[str]:
     def check(path):
         return (file_ok and os.path.isfile(path)) or (dir_ok and os.path.isdir(root_path))
diff --git a/k8t/secret_providers.py b/k8t/secret_providers.py
index ca07b08..5e0bb21 100644
--- a/k8t/secret_providers.py
+++ b/k8t/secret_providers.py
@@ -13,38 +13,38 @@ import string
 import boto3
 
 try:
-    from secrets import choice
+    from secrets import SystemRandom
 except ImportError:
     from random import SystemRandom
 
-    choice = SystemRandom().choice
-
-
 
 LOGGER = logging.getLogger(__name__)
+RANDOM_STORE = {}
 
 
-def ssm(key: str) -> str:
+def ssm(key: str, length: int = None) -> str:
     LOGGER.debug("Requesting secret from %s", key)
 
     client = boto3.client("ssm")
 
     try:
-        return client.get_parameter(Name="/{}".format(key), WithDecryption=True)["Parameter"][
-            "Value"
-        ]
+        result = client.get_parameter(Name="/{}".format(key), WithDecryption=True)["Parameter"]["Value"]
+
+        if length is not None:
+            if len(result) != length:
+                raise AssertionError("Secret '{}' did not have expected length of {}".format(key, length))
+
+        return result
     except client.exceptions.ParameterNotFound:
         raise RuntimeError("Could not find secret: {}".format(key))
 
 
-RANDOM_STORE = {}
-
-def random(key: str) -> str:
+def random(key: str, length: int = None) -> str:
     LOGGER.debug("Requesting secret from %s", key)
 
     if key not in RANDOM_STORE:
         RANDOM_STORE[key] = "".join(
-            choice(string.ascii_lowercase + string.digits) for _ in range(24)
+            SystemRandom().choice(string.ascii_lowercase + string.digits) for _ in range(length or SystemRandom().randint(12, 32))
         )
 
     return RANDOM_STORE[key]
diff --git a/k8t/secrets.py b/k8t/secrets.py
deleted file mode 100644
index 241f066..0000000
--- a/k8t/secrets.py
+++ /dev/null
@@ -1,29 +0,0 @@
-# -*- coding: utf-8 -*-
-# ISC License
-#
-# Copyright 2019 FL Fintech E GmbH
-#
-# Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-from k8t import secret_providers
-from k8t.config import load_all
-
-
-def get_secret(key: str, path: str, cluster: str, environment: str) -> str:
-    config = load_all(path, cluster, environment, 'ltr')
-
-    if "secrets" not in config:
-        raise RuntimeError(
-            "No configuration for secrets found: {}".format(config))
-
-    try:
-        provider = getattr(secret_providers, config["secrets"]["provider"].lower())
-    except AttributeError:
-        raise NotImplementedError("secret provider {} does not exist.".format(config["secrets"]["provider"].lower()))
-
-    return provider(
-        "{0}/{1}".format(config['secrets']['prefix'],
-                         key) if "prefix" in config["secrets"] else key
-    )
diff --git a/k8t/templates.py b/k8t/templates.py
index c4e0b2b..485485d 100644
--- a/k8t/templates.py
+++ b/k8t/templates.py
@@ -13,6 +13,8 @@ from typing import Set, Tuple
 
 from jinja2 import meta, nodes
 
+from k8t import config
+
 LOGGER = logging.getLogger(__name__)
 PROHIBITED_VARIABLE_NAMES = {
     'namespace',
@@ -43,7 +45,7 @@ def analyze(template_path: str, values: dict, engine) -> Tuple[Set[str], Set[str
     return (undefined_variables - invalid_variables), unused_variables, invalid_variables, secrets
 
 
-def validate(template_path: str, values: dict, engine, config) -> bool:
+def validate(template_path: str, values: dict, engine) -> bool:
     config_ok = True
     undefined, _, invalid, secrets = analyze(template_path, values, engine)
 
@@ -56,9 +58,9 @@ def validate(template_path: str, values: dict, engine, config) -> bool:
             "Invalid variable names found: %s", sorted(invalid))
 
     if secrets:
-        if "secrets" not in config:
+        if "secrets" not in config.CONFIG:
             LOGGER.error(
-                "No configuration for secrets found: %s", config)
+                "No configuration for secrets found: %s", config.CONFIG)
             config_ok = False
 
     return config_ok and not (invalid or undefined)

allow specifying secret length for random provider
need to check how integration with other providers would work (maybe an assertion for validating the secret?)
diff --git a/tests/config.py b/tests/config.py
index 28c341a..f36f49d 100644
--- a/tests/config.py
+++ b/tests/config.py
@@ -7,13 +7,4 @@
 #
 # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 
-from k8t.config import validate
-
-
-def test_validate():
-    try:
-        validate(dict(secrets=dict()))
-    except AssertionError:
-        return
-
-    assert False, "validate should raise AssertionError when no secrets provider was defined"
+from k8t import config
diff --git a/tests/secret_providers.py b/tests/secret_providers.py
index 6e2f75c..49fc2e2 100644
--- a/tests/secret_providers.py
+++ b/tests/secret_providers.py
@@ -11,10 +11,12 @@ from k8t.secret_providers import random
 
 
 def test_random():
-    assert random('/foobar') == random('/foobar') != random('/foobaz')
+    length = 12
 
-    result = random('/foobam')
+    assert random('/foobar', length) == random('/foobar', length) != random('/foobaz', length)
 
-    assert result == random('/foobam')
+    result = random('/foobam', length)
+
+    assert result == random('/foobam', length)
 
 # vim: fenc=utf-8:ts=4:sw=4:expandtab

pip install -e .[dev]
parse_log_pytest
pytest
pytest
apt-get update apt-get install -y gcc
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
boto3==1.37.23
botocore==1.37.23
click==8.1.8
coloredlogs==15.0.1
exceptiongroup @ file:///croot/exceptiongroup_1706031385326/work
humanfriendly==10.0
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
Jinja2==3.1.6
jmespath==1.0.1
-e git+https://github.com/ClarkSource/k8t.git@7a7c69eb05c38cfa4d07bb9da65984cdd430678b#egg=k8t
MarkupSafe==3.0.2
packaging @ file:///croot/packaging_1734472117206/work
pluggy @ file:///croot/pluggy_1733169602837/work
pytest @ file:///croot/pytest_1738938843180/work
python-dateutil==2.9.0.post0
PyYAML==6.0.2
s3transfer==0.11.4
simple_tools==0.2.0.post2
six==1.17.0
termcolor==3.0.0
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
urllib3==1.26.20

name: k8t
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - exceptiongroup=1.2.0=py39h06a4308_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - packaging=24.2=py39h06a4308_0
  - pip=25.0=py39h06a4308_0
  - pluggy=1.5.0=py39h06a4308_0
  - pytest=8.3.4=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py39h06a4308_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - boto3==1.37.23
      - botocore==1.37.23
      - click==8.1.8
      - coloredlogs==15.0.1
      - humanfriendly==10.0
      - jinja2==3.1.6
      - jmespath==1.0.1
      - markupsafe==3.0.2
      - python-dateutil==2.9.0.post0
      - pyyaml==6.0.2
      - s3transfer==0.11.4
      - simple-tools==0.2.0.post2
      - six==1.17.0
      - termcolor==3.0.0
      - urllib3==1.26.20
prefix: /opt/conda/envs/k8t

tests/secret_providers.py::test_random
k8t/secrets.py k8t/environment.py examples/single-cluster/templates/hello-world-secret.yaml.j2 k8t/engine.py k8t/templates.py k8t/config.py k8t/cli.py k8t/filters.py k8t/project.py k8t/secret_providers.py
k8t/secrets.py k8t/environment.py examples/single-cluster/templates/hello-world-secret.yaml.j2 k8t/engine.py k8t/templates.py k8t/config.py k8t/cli.py k8t/filters.py k8t/project.py k8t/secret_providers.py