diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index fb1ed91402a9ba400dbb69ccf4744b49ae0ea3df..0592efc9de3f8644ca0f141a555c389d6069359b 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -5,4 +5,4 @@ stages:
   - build
 
 variables:
-  GITLAB_CI_FILES: "templates/*.yml"
+  GITLAB_CI_FILES: "templates/extract.yml templates/validation.yml"
diff --git a/README.md b/README.md
index c749a260f98c90906b27ff4f1e32a9a00079b4ff..1057a17bb3545db4e7c5a98723ede733862b8e06 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
 
 This template provides a jobs to validate your template syntax.
 
-It uses the [GitLab CI Lint API](https://docs.gitlab.com/ee/api/lint.html).
+It uses the [GitLab CI Lint API](https://docs.gitlab.com/api/lint/).
 
 ## Template validation
 
@@ -24,3 +24,61 @@ stages:
 variables:
   GITLAB_CI_FILES: "templates/*.yml"
 ```
+## Variants
+
+The default validation template is designed to work on untagged runners, without any proxy configuration using Docker images
+from the internet and using Default Trusted Certificate Authorities.
+
+Nevertheless there are template variants available to cover specific cases.
+
+### Vault variant
+
+This variant allows delegating your secrets management to a [Vault](https://www.vaultproject.io/) server.
+
+#### Configuration
+
+In order to be able to communicate with the Vault server, the variant requires the additional configuration parameters:
+
+| Input / Variable | Description                            | Default value     |
+| ----------------- | -------------------------------------- | ----------------- |
+| `TBC_VAULT_IMAGE` | The [Vault Secrets Provider](https://gitlab.com/to-be-continuous/tools/vault-secrets-provider) image to use (can be overridden) | `registry.gitlab.com/to-be-continuous/tools/vault-secrets-provider:latest` |
+| `vault-base-url` / `VAULT_BASE_URL` | The Vault server base API url          | **must be defined** |
+| `vault-oidc-aud` / `VAULT_OIDC_AUD` | The `aud` claim for the JWT | `$CI_SERVER_URL` |
+| :lock: `VAULT_ROLE_ID`   | The [AppRole](https://www.vaultproject.io/docs/auth/approle) RoleID | _none_ |
+| :lock: `VAULT_SECRET_ID` | The [AppRole](https://www.vaultproject.io/docs/auth/approle) SecretID | _none_ |
+
+By default, the variant will authentifacte using a [JWT ID token](https://docs.gitlab.com/ci/secrets/id_token_authentication/). To use [AppRole](https://www.vaultproject.io/docs/auth/approle) instead the `VAULT_ROLE_ID` and `VAULT_SECRET_ID` should be defined as secret project variables.
+
+#### Usage
+
+Then you may retrieve any of your secret(s) from Vault using the following syntax:
+
+```text
+@url@http://vault-secrets-provider/api/secrets/{secret_path}?field={field}
+```
+
+With:
+
+| Parameter                        | Description                            |
+| -------------------------------- | -------------------------------------- |
+| `secret_path` (_path parameter_) | this is your secret location in the Vault server |
+| `field` (_query parameter_)      | parameter to access a single basic field from the secret JSON payload |
+
+#### Example
+
+```yaml
+include:
+  # main template
+  - component: $CI_SERVER_FQDN/to-be-continuous/tools/gitlab-ci/validation@master
+  # Vault variant
+  - component: $CI_SERVER_FQDN/to-be-continuous/tools/gitlab-ci/validation-vault@master
+    inputs:
+      # audience claim for JWT
+      vault-oidc-aud: "https://vault.acme.host"
+      vault-base-url: "https://vault.acme.host/v1"
+
+variables:
+  # Secrets managed by Vault
+  GITLAB_TOKEN: "@url@http://vault-secrets-provider/api/secrets/b7ecb6ebabc231/my-infra/gitlab?field=token"
+```
+
diff --git a/templates/validation-vault.yml b/templates/validation-vault.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e76fe9d5a3be7f5b8489baca69eebce2d7908c24
--- /dev/null
+++ b/templates/validation-vault.yml
@@ -0,0 +1,31 @@
+ # ====================================================================================================================
+ # === Vault template variant
+ # ====================================================================================================================
+spec:
+  inputs:
+    vault-base-url:
+      description: The Vault server base API url
+      default: ''
+    vault-oidc-aud:
+      description: the `aud` claim for the JWT
+      default: $CI_SERVER_URL
+---
+variables:
+  # variabilized vault-secrets-provider image
+  TBC_VAULT_IMAGE: registry.gitlab.com/to-be-continuous/tools/vault-secrets-provider:latest
+  # variables have to be explicitly declared in the YAML to be exported to the service
+  VAULT_ROLE_ID: $VAULT_ROLE_ID
+  VAULT_SECRET_ID: $VAULT_SECRET_ID
+  VAULT_OIDC_AUD: $[[ inputs.vault-oidc-aud ]]
+
+  VAULT_BASE_URL: $[[ inputs.vault-base-url ]]
+
+.gitlab-ci-base:
+  services:
+    - name: "$TBC_VAULT_IMAGE"
+      alias: "vault-secrets-provider"
+  variables:
+    VAULT_JWT_TOKEN: "$VAULT_JWT_TOKEN"
+  id_tokens:
+    VAULT_JWT_TOKEN:
+      aud: "$VAULT_OIDC_AUD"
diff --git a/templates/validation.yml b/templates/validation.yml
index d1ab3583923a4c2b1c94385c6c3d109f5cfeca1d..4f2361fd09dc6cd3f044f962377f99c188a40893 100644
--- a/templates/validation.yml
+++ b/templates/validation.yml
@@ -33,6 +33,11 @@ spec:
     echo -e "[\e[1;91mERROR\e[0m] $*" >&2
   }
 
+  function fail() {
+    log_error "$*"
+    exit 1
+  }
+
   function install_ca_certs() {
     certs=$1
     if [[ -z "$certs" ]]
@@ -51,6 +56,159 @@ spec:
     fi
   }
 
+  function unscope_variables() {
+    _scoped_vars=$(env | awk -F '=' "/^scoped__[a-zA-Z0-9_]+=/ {print \$1}" | sort)
+    if [[ -z "$_scoped_vars" ]]; then return; fi
+    log_info "Processing scoped variables..."
+    for _scoped_var in $_scoped_vars
+    do
+      _fields=${_scoped_var//__/:}
+      _condition=$(echo "$_fields" | cut -d: -f3)
+      case "$_condition" in
+      if) _not="";;
+      ifnot) _not=1;;
+      *)
+        log_warn "... unrecognized condition \\e[1;91m$_condition\\e[0m in \\e[33;1m${_scoped_var}\\e[0m"
+        continue
+      ;;
+      esac
+      _target_var=$(echo "$_fields" | cut -d: -f2)
+      _cond_var=$(echo "$_fields" | cut -d: -f4)
+      _cond_val=$(eval echo "\$${_cond_var}")
+      _test_op=$(echo "$_fields" | cut -d: -f5)
+      case "$_test_op" in
+      defined)
+        if [[ -z "$_not" ]] && [[ -z "$_cond_val" ]]; then continue; 
+        elif [[ "$_not" ]] && [[ "$_cond_val" ]]; then continue; 
+        fi
+        ;;
+      equals|startswith|endswith|contains|in|equals_ic|startswith_ic|endswith_ic|contains_ic|in_ic)
+        # comparison operator
+        # sluggify actual value
+        _cond_val=$(echo "$_cond_val" | tr '[:punct:]' '_')
+        # retrieve comparison value
+        _cmp_val_prefix="scoped__${_target_var}__${_condition}__${_cond_var}__${_test_op}__"
+        _cmp_val=${_scoped_var#"$_cmp_val_prefix"}
+        # manage 'ignore case'
+        if [[ "$_test_op" == *_ic ]]
+        then
+          # lowercase everything
+          _cond_val=$(echo "$_cond_val" | tr '[:upper:]' '[:lower:]')
+          _cmp_val=$(echo "$_cmp_val" | tr '[:upper:]' '[:lower:]')
+        fi
+        case "$_test_op" in
+        equals*)
+          if [[ -z "$_not" ]] && [[ "$_cond_val" != "$_cmp_val" ]]; then continue; 
+          elif [[ "$_not" ]] && [[ "$_cond_val" == "$_cmp_val" ]]; then continue; 
+          fi
+          ;;
+        startswith*)
+          if [[ -z "$_not" ]] && [[ "$_cond_val" != "$_cmp_val"* ]]; then continue; 
+          elif [[ "$_not" ]] && [[ "$_cond_val" == "$_cmp_val"* ]]; then continue; 
+          fi
+          ;;
+        endswith*)
+          if [[ -z "$_not" ]] && [[ "$_cond_val" != *"$_cmp_val" ]]; then continue; 
+          elif [[ "$_not" ]] && [[ "$_cond_val" == *"$_cmp_val" ]]; then continue; 
+          fi
+          ;;
+        contains*)
+          if [[ -z "$_not" ]] && [[ "$_cond_val" != *"$_cmp_val"* ]]; then continue; 
+          elif [[ "$_not" ]] && [[ "$_cond_val" == *"$_cmp_val"* ]]; then continue; 
+          fi
+          ;;
+        in*)
+          if [[ -z "$_not" ]] && [[ "__${_cmp_val}__" != *"__${_cond_val}__"* ]]; then continue; 
+          elif [[ "$_not" ]] && [[ "__${_cmp_val}__" == *"__${_cond_val}__"* ]]; then continue; 
+          fi
+          ;;
+        esac
+        ;;
+      *)
+        log_warn "... unrecognized test operator \\e[1;91m${_test_op}\\e[0m in \\e[33;1m${_scoped_var}\\e[0m"
+        continue
+        ;;
+      esac
+      # matches
+      _val=$(eval echo "\$${_target_var}")
+      log_info "... apply \\e[32m${_target_var}\\e[0m from \\e[32m\$${_scoped_var}\\e[0m${_val:+ (\\e[33;1moverwrite\\e[0m)}"
+      _val=$(eval echo "\$${_scoped_var}")
+      export "${_target_var}"="${_val}"
+    done
+    log_info "... done"
+  }
+
+  # evaluate and export a secret
+  # - $1: secret variable name
+  function eval_secret() {
+    name=$1
+    value=$(eval echo "\$${name}")
+    case "$value" in
+    @b64@*)
+      decoded=$(mktemp)
+      errors=$(mktemp)
+      if echo "$value" | cut -c6- | base64 -d > "${decoded}" 2> "${errors}"
+      then
+        # shellcheck disable=SC2086
+        export ${name}="$(cat ${decoded})"
+        log_info "Successfully decoded base64 secret \\e[33;1m${name}\\e[0m"
+      else
+        fail "Failed decoding base64 secret \\e[33;1m${name}\\e[0m:\\n$(sed 's/^/... /g' "${errors}")"
+      fi
+      ;;
+    @hex@*)
+      decoded=$(mktemp)
+      errors=$(mktemp)
+      if echo "$value" | cut -c6- | sed 's/\([0-9A-F]\{2\}\)/\\\\x\1/gI' | xargs printf > "${decoded}" 2> "${errors}"
+      then
+        # shellcheck disable=SC2086
+        export ${name}="$(cat ${decoded})"
+        log_info "Successfully decoded hexadecimal secret \\e[33;1m${name}\\e[0m"
+      else
+        fail "Failed decoding hexadecimal secret \\e[33;1m${name}\\e[0m:\\n$(sed 's/^/... /g' "${errors}")"
+      fi
+      ;;
+    @url@*)
+      url=$(echo "$value" | cut -c6-)
+      if command -v curl > /dev/null
+      then
+        decoded=$(mktemp)
+        errors=$(mktemp)
+        if curl -s -S -f --connect-timeout 5 -o "${decoded}" "$url" 2> "${errors}"
+        then
+          # shellcheck disable=SC2086
+          export ${name}="$(cat ${decoded})"
+          log_info "Successfully curl'd secret \\e[33;1m${name}\\e[0m"
+        else
+          log_warn "Failed getting secret \\e[33;1m${name}\\e[0m:\\n$(sed 's/^/... /g' "${errors}")"
+        fi
+      elif command -v wget > /dev/null
+      then
+        decoded=$(mktemp)
+        errors=$(mktemp)
+        if wget -T 5 -O "${decoded}" "$url" 2> "${errors}"
+        then
+          # shellcheck disable=SC2086
+          export ${name}="$(cat ${decoded})"
+          log_info "Successfully wget'd secret \\e[33;1m${name}\\e[0m"
+        else
+          log_warn "Failed getting secret \\e[33;1m${name}\\e[0m:\\n$(sed 's/^/... /g' "${errors}")"
+        fi
+      else
+        log_warn "Couldn't get secret \\e[33;1m${name}\\e[0m: no http client found"
+      fi
+      ;;
+    esac
+  }
+
+  function eval_all_secrets() {
+    encoded_vars=$(env | grep -Ev '(^|.*_ENV_)scoped__' | awk -F '=' '/^[a-zA-Z0-9_]*=@(b64|hex|url)@/ {print $1}')
+    for var in $encoded_vars
+    do
+      eval_secret "$var"
+    done
+  }
+
   # validates an input GitLab CI YAML file
   function ci_lint() {
     rc=0
@@ -71,12 +229,18 @@ spec:
     exit $rc
   }
 
-gitlab-ci-lint:
-  image: registry.hub.docker.com/badouralix/curl-jq:latest
-  stage: build
+  unscope_variables
+  eval_all_secrets
+
+.gitlab-ci-base:
   before_script:
     - !reference [.lint-scripts]
     - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}"
+
+gitlab-ci-lint:
+  extends: .gitlab-ci-base
+  stage: build
+  image: registry.hub.docker.com/badouralix/curl-jq:latest
   script:
     - ci_lint
   rules: