diff --git a/README.md b/README.md
index 7d023c6f1c4ab50ad8f6fad04a810fc42d1303c9..b729020d4415d393fd88c53d3c90661cc133ca18 100644
--- a/README.md
+++ b/README.md
@@ -144,7 +144,7 @@ Examples (with an application's base name `myapp`):
 The Kubernetes template supports three techniques to deploy your code:
 
 1. script-based deployment,
-2. template-based deployment using raw Kubernetes manifests (with variables substitution),
+2. template-based deployment using raw Kubernetes manifests (with [variables substitution](#variables-substitution-mechanism)),
 3. template-based deployment using [Kustomization files](https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/).
 
 #### 1: script-based deployment
@@ -167,7 +167,7 @@ in your project structure, and let the GitLab CI template [`kubectl apply`](http
 The template processes the following steps:
 
 1. _optionally_ executes the `k8s-pre-apply.sh` script in your project to perform specific environment pre-initialization (for e.g. create required services),
-2. looks for your Kubernetes deployment file, performs [variables substitution](#using-variables) and [`kubectl apply`](https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands#apply) it,
+2. looks for your Kubernetes deployment file, performs [variables substitution](#variables-substitution-mechanism) and [`kubectl apply`](https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands#apply) it,
     1. look for a specific `deployment-$environment_type.yml` in your project (e.g. `deployment-staging.yml` for staging environment),
     2. fallbacks to default `deployment.yml`.
 3. _optionally_ executes the `k8s-post-apply.sh` script in your project to perform specific environment post-initialization stuff,
@@ -202,7 +202,7 @@ After deployment (either script-based or template-based), the GitLab CI template
 The Kubernetes template supports three techniques to destroy an environment (actually only review environments):
 
 1. script-based deployment,
-2. template-based deployment using raw Kubernetes manifests (with variables substitution),
+2. template-based deployment using raw Kubernetes manifests (with [variables substitution](#variables-substitution-mechanism)),
 3. template-based deployment using [Kustomization files](https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/).
 
 #### 1: script-based cleanup
@@ -282,41 +282,32 @@ by using available environment variables:
 3. any [custom variable](https://docs.gitlab.com/ee/ci/variables/#for-a-project)
     (ex: `${SECRET_TOKEN}` that you have set in your project CI/CD variables)
 
-While your scripts may simply use any of those variables, your Kubernetes and Kustomize resources can use **variable substitution**
-with the syntax `${VARIABLE_NAME}`.
-Each of those patterns will be dynamically replaced in your resources by the template right before using it.
-
-You can prevent any line from being processed by appending `# nosubst` at the end of the line.
-For instance in the following example, `${REMOTE_SERVICE_NAME}` won't be replaced by its environment value during GitLab job execution:
-
-```yaml
-apiVersion: v1
-kind: ConfigMap
-metadata:
-  labels:
-    app: ${APPLICATION_NAME}
-  name: ${APPLICATION_NAME}
-data:
-  application.yml: |
-    remote:
-      some-service:
-          name: '${REMOTE_SERVICE_NAME}' # nosubst
-```
-
-> :warning: In order to be properly replaced, curly braces are mandatory (ex: `${MYVAR}` and not `$MYVAR`).
-> Moreover, multiline variables must be surrounded by **double quotes** (`"`).
->
-> Example:
->
-> ```yaml
-> [...]
-> containers:
-> - name: restaurant-app
->   env:
->   # multiline variable
->   - name: MENU
->     value: "${APP_MENU}"
-> ```
+#### Variables substitution mechanism
+
+While your scripts may freely use any of the available variables, your Kubernetes and Kustomize
+resources can use a **variables substitution** mechanism implemented by the template:
+
+- Using the syntax `${VARIABLE_NAME}` or `%{VARIABLE_NAME}`.\
+  :warning: Curly braces (`{}`) are mandatory in the expression (`$VARIABLE_NAME` won't be processed).
+- Each of those expressions will be **dynamically expanded** in your resource files with the variable value, right before being used.
+- Variable substitution expressions **must be contained in double-quoted strings**.
+  The substitution implementation takes care of escaping characters that need to be (double quote `"`, backslash `\`, tab `\t`, carriage return `\n` and line feed `\r`).
+- Variable substitution can be prevented by appending `# nosubst` at the end of any line.\
+  Ex:
+  ```yaml
+  apiVersion: v1
+  kind: ConfigMap
+  metadata:
+    # ${environment_name} will be expanded
+    labels:
+      app: "${environment_name}"
+    name: "${environment_name}"
+  data:
+    application.yml: |
+      remote:
+        some-service:
+            name: '${REMOTE_SERVICE_NAME}' # nosubst
+  ```
 
 ### Environments URL management
 
diff --git a/templates/gitlab-ci-k8s.yml b/templates/gitlab-ci-k8s.yml
index 8d69879a97620beaa57527d3c0179411322208d7..f6bfa1e1abc92f54eae90d5546e140954ab00454 100644
--- a/templates/gitlab-ci-k8s.yml
+++ b/templates/gitlab-ci-k8s.yml
@@ -419,9 +419,78 @@ stages:
     echo "$1" | tr '[:lower:]' '[:upper:]' | tr '[:punct:]' '_'
   }
 
-  function awkenvsubst() {
-    # performs variables escaping: '&' for gsub + JSON chars ('\' and '"')
-    awk '!/# *nosubst/{while(match($0,"[$%]{[^}]*}")) {var=substr($0,RSTART+2,RLENGTH-3);val=ENVIRON[var];gsub(/["\\&]/,"\\\\&",val);gsub("[$%]{"var"}",val)}}1'
+  function tbc_envsubst() {
+    awk '
+      BEGIN {
+        count_replaced_lines = 0
+        # ASCII codes
+        for (i=0; i<=255; i++)
+          char2code[sprintf("%c", i)] = i
+      }
+      # determine encoding (from env or from file extension)
+      function encoding() {
+        enc = ENVIRON["TBC_ENVSUBST_ENCODING"]
+        if (enc != "")
+          return enc
+        if (match(FILENAME, /\.(json|yaml|yml)$/))
+          return "jsonstr"
+        return "raw"
+      }
+      # see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
+      function uriencode(str) {
+        len = length(str)
+        enc = ""
+        for (i=1; i<=len; i++) {
+          c = substr(str, i, 1);
+          if (index("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.!~*'\''()", c))
+            enc = enc c
+          else
+            enc = enc "%" sprintf("%02X", char2code[c])
+        }
+        return enc
+      }
+      !/# *nosubst/ {
+        orig_line = $0
+        line = $0
+        count_repl_in_line = 0
+        # /!\ 3rd arg (match) not supported in BusyBox awk
+        while (match(line, /[$%]\{([[:alnum:]_]+)\}/)) {
+          expr_start = RSTART
+          expr_len = RLENGTH
+          # get var name
+          var = substr(line, expr_start+2, expr_len-3)
+          # get var value (from env)
+          val = ENVIRON[var]
+          # check variable is set
+          if (val == "") {
+            printf("[\033[1;93mWARN\033[0m] Environment variable \033[33;1m%s\033[0m is not set or empty\n", var) > "/dev/stderr"
+          } else {
+            enc = encoding()
+            if (enc == "jsonstr") {
+              gsub(/["\\]/, "\\\\&", val)
+              gsub("\n", "\\n", val)
+              gsub("\r", "\\r", val)
+              gsub("\t", "\\t", val)
+            } else if (enc == "uricomp") {
+              val = uriencode(val)
+            } else if (enc == "raw") {
+            } else {
+              printf("[\033[1;93mWARN\033[0m] Unsupported encoding \033[33;1m%s\033[0m: ignored\n", enc) > "/dev/stderr"
+            }
+          }
+          # replace expression in line
+          line = substr(line, 1, expr_start - 1) val substr(line, expr_start + expr_len)
+          count_repl_in_line++
+        }
+        if (count_repl_in_line) {
+          if (count_replaced_lines == 0)
+            printf("[\033[1;94mINFO\033[0m] Variable expansion occurred in file \033[33;1m%s\033[0m:\n", FILENAME) > "/dev/stderr"
+          count_replaced_lines++
+          printf("> line %s: %s\n", NR, orig_line) > "/dev/stderr"
+        }
+        print line
+      }
+    ' "$@"
   }
 
   function exec_hook() {
@@ -539,8 +608,8 @@ stages:
         exit 1
       fi
 
-      # replace variables (alternative for envsubst which is not present in image)
-      awkenvsubst < "$deploymentfile" > generated-deployment.yml
+      # variables substitution
+      tbc_envsubst "$deploymentfile" > generated-deployment.yml
 
       log_info "--- \\e[32mkubectl $action\\e[0m"
       kubectl ${TRACE+-v=5} "$action" -f ./generated-deployment.yml
@@ -562,7 +631,7 @@ stages:
     export appname_ssc=$environment_name_ssc
 
     # variables expansion in $environment_url
-    environment_url=$(echo "$environment_url" | awkenvsubst)
+    environment_url=$(echo "$environment_url" | TBC_ENVSUBST_ENCODING=uricomp tbc_envsubst)
     export environment_url
     # extract hostname from $environment_url
     hostname=$(echo "$environment_url" | awk -F[/:] '{print $4}')
@@ -644,7 +713,7 @@ stages:
     export appname_ssc=$environment_name_ssc
 
     # variables expansion in $environment_url
-    environment_url=$(echo "$environment_url" | awkenvsubst)
+    environment_url=$(echo "$environment_url" | TBC_ENVSUBST_ENCODING=uricomp tbc_envsubst)
     export environment_url
     # extract hostname from $environment_url
     hostname=$(echo "$environment_url" | awk -F[/:] '{print $4}')
@@ -755,8 +824,8 @@ stages:
         exit 1
       fi
 
-      # replace variables (alternative for envsubst which is not present in image)
-      awkenvsubst < "$deploymentfile" > generated-deployment.yml
+      # variables substitution
+      tbc_envsubst "$deploymentfile" > generated-deployment.yml
 
       # shellcheck disable=SC2086
       /usr/bin/kube-score score $K8S_SCORE_EXTRA_OPTS generated-deployment.yml
@@ -764,7 +833,7 @@ stages:
   }
 
   # export tool functions (might be used in after_script)
-  export -f log_info log_warn log_error assert_defined rollback awkenvsubst
+  export -f log_info log_warn log_error assert_defined rollback tbc_envsubst
 
   unscope_variables
   eval_all_secrets