Index: /branches/amp_4_0/platform/tools/container/.env
===================================================================
--- /branches/amp_4_0/platform/tools/container/.env	(revision 2934)
+++ /branches/amp_4_0/platform/tools/container/.env	(working copy)
@@ -36,6 +36,11 @@
 # Uncomment to override auto-detection (e.g. for specific domain or IP).
 # AMP_DOMAIN_OR_IP=192.168.162.139
 
+# --- AMP WebUI Session Timeout (Idle) ---
+# Session expires after this many idle minutes.
+AMP_SSO_TOKEN_TTL_MINUTES=120
+AMP_SESSION_IDLE_TIMEOUT_MINUTES=120
+
 # --- OpenSearch Memory Allocation ---
 # By default, manage_amp.sh calculates this based on 50% of Host RAM.
 # Uncomment to override (e.g. for testing constraints).
Index: /branches/amp_4_0/platform/tools/container/DEPLOYMENT.md
===================================================================
--- /branches/amp_4_0/platform/tools/container/DEPLOYMENT.md	(revision 2934)
+++ /branches/amp_4_0/platform/tools/container/DEPLOYMENT.md	(working copy)
@@ -152,6 +152,20 @@
 ```
 
 This waits for services to be ready and initializes security, databases, and dashboards.
+
+**Engineer shortcut (single command):**
+
+```bash
+./manage_amp.sh deploy --auto --post
+```
+
+**Alternate shortcut:**
+
+```bash
+./manage_amp.sh deploy_full
+```
+
+> Note: Post-deploy includes built-in waits and retries for OpenSearch, Dashboards, and Postgres.
 
 ### Step 7: Verify Deployment
 
@@ -161,6 +175,20 @@
 
 Access the GUI at `https://<NODE_IP>/` or `https://<VIP>/`
 
+### Common Operations (Minimal Commands)
+
+Restart all services:
+
+```bash
+./manage_amp.sh restart
+```
+
+Restart a single service (example: OpenSearch):
+
+```bash
+./manage_amp.sh restart opensearch
+```
+
 ---
 
 ## Part 3: Offline Deployment (Air-Gapped)
Index: /branches/amp_4_0/platform/tools/container/manage_amp.sh
===================================================================
--- /branches/amp_4_0/platform/tools/container/manage_amp.sh	(revision 2934)
+++ /branches/amp_4_0/platform/tools/container/manage_amp.sh	(working copy)
@@ -820,6 +820,26 @@
         fi
 
         echo "✅ Generated $HP_OUTPUT"
+    fi
+
+    # Generate Grafana JWT JWKS from the shared OPENSEARCH_JWT_SECRET
+    GRAFANA_JWT_DIR="$SERVICES_DIR/grafana/jwt"
+    GRAFANA_JWKS_FILE="$GRAFANA_JWT_DIR/jwks.json"
+    mkdir -p "$GRAFANA_JWT_DIR"
+
+    if [ -z "$OPENSEARCH_JWT_SECRET" ]; then
+        echo "⚠️  OPENSEARCH_JWT_SECRET is empty; skipping Grafana JWKS generation."
+    else
+        if command -v openssl >/dev/null 2>&1; then
+            SECRET_B64URL=$(printf '%s' "$OPENSEARCH_JWT_SECRET" | openssl base64 -A | tr '+/' '-_' | tr -d '=')
+        else
+            SECRET_B64URL=$(printf '%s' "$OPENSEARCH_JWT_SECRET" | base64 | tr -d '\n=' | tr '+/' '-_')
+        fi
+
+        cat > "$GRAFANA_JWKS_FILE" <<EOF
+{"keys":[{"kty":"oct","kid":"amp-grafana-hs256","use":"sig","alg":"HS256","k":"$SECRET_B64URL"}]}
+EOF
+        echo "✅ Generated $GRAFANA_JWKS_FILE"
     fi
 }
 
@@ -1425,6 +1445,9 @@
     echo "  sudo useradd -m -s /ca/bin/ca_shell amp_operator"
     echo "  sudo passwd amp_operator"
     echo ""
+    echo "Next step for end users:"
+    echo "  ./manage_amp.sh post_deploy"
+    echo ""
 }
 
 rm_stack() {
@@ -1434,6 +1457,8 @@
 
 run_setup() {
     echo "--- Running Setup (Generating Certificates) ---"
+    # Ensure secrets are loaded so JWT signing key is consistent
+    load_secrets_file
     # Ensure volumes exist
     if ! docker volume ls | grep -q "certs-vol"; then
         docker volume create certs-vol
@@ -1564,6 +1589,8 @@
     echo "--- Running Configurator (OpenSearch Setup) ---"
     
     check_swarm
+    # Ensure secrets are loaded for standalone configurator runs.
+    load_secrets_file
     
     # Find a running OpenSearch container (it already has curl installed)
     CONTAINER_ID=$(docker ps --format '{{.ID}} {{.Names}}' | grep "amp_opensearch" | grep -v "dashboards" | awk '{print $1}' | head -n 1)
@@ -2045,7 +2072,38 @@
     echo "✅ Post-Deployment Configuration Complete!"
     echo "========================================================="
 }
+
+restart_services() {
+    check_swarm
 
+    local target="$1"
+    local namespace="com.docker.stack.namespace=${STACK_NAME}"
+
+    if [ -n "$target" ]; then
+        # Allow short names (e.g., opensearch) or full service names (e.g., amp_opensearch)
+        local service_name="$target"
+        if ! docker service ls --format '{{.Name}}' | grep -q "^${service_name}$"; then
+            service_name="${STACK_NAME}_${target}"
+        fi
+        if ! docker service ls --format '{{.Name}}' | grep -q "^${service_name}$"; then
+            echo "❌ Service not found: $target"
+            exit 1
+        fi
+        echo "Restarting service: $service_name"
+        docker service update --force "$service_name"
+        echo "✅ Restart requested for $service_name"
+        return
+    fi
+
+    echo "Restarting all services in stack: $STACK_NAME"
+    docker service ls --filter "label=$namespace" --format '{{.Name}}' | while read -r svc; do
+        [ -n "$svc" ] || continue
+        echo "  - $svc"
+        docker service update --force "$svc" >/dev/null
+    done
+    echo "✅ Restart requested for all services in $STACK_NAME"
+}
+
 case $ACTION in
     init)
         init_swarm "$2"  # Pass optional advertise-addr as second argument
@@ -2077,10 +2135,18 @@
         ;;
     deploy)
         if [ "$2" == "--auto" ]; then
-             auto_configure
+            auto_configure
         fi
         deploy_stack
+        if [ "$2" == "--post" ] || [ "$3" == "--post" ]; then
+            post_deploy_config
+        fi
         ;;
+    deploy_full)
+        auto_configure
+        deploy_stack
+        post_deploy_config
+        ;;
     auto-config)
         auto_configure
         ;;
@@ -2104,6 +2170,9 @@
     rotate-secrets)
         rotate_secrets "$2"
         ;;
+    restart)
+        restart_services "$2"
+        ;;
     rm|remove)
         rm_stack
         ;;
@@ -2111,11 +2180,15 @@
         docker stack services $STACK_NAME
         ;;
     *)
-        echo "Usage: $0 {init|init-secrets|build|bundle|load_offline|setup|deploy|post_deploy|security_init|status|configurator|system_tune|rotate-secrets|rm|vip}"
+        echo "Usage: $0 {init|init-secrets|build|bundle|load_offline|setup|deploy|deploy_full|post_deploy|security_init|status|configurator|system_tune|rotate-secrets|restart|rm|vip}"
         echo "  init-secrets        : Interactive password setup (before first deploy)"
         echo "  rotate-secrets      : Rotate passwords (after stopping stack)"
         echo "  deploy --auto       : Auto-configure cluster and deploy"
+        echo "  deploy --post       : Deploy and run post-deploy steps"
+        echo "  deploy --auto --post: Auto-configure, deploy, then post-deploy"
+        echo "  deploy_full         : Same as --auto --post"
         echo "  post_deploy         : Run all post-deployment configuration steps (Wait & Init)"
+        echo "  restart [service]   : Restart all services or a single service (e.g., opensearch)"
         echo "  vip commands        : ./manage_amp.sh vip --vip <IP> --priority <INT> [--interface <IFACE>]"
         exit 1
 esac
Index: /branches/amp_4_0/platform/tools/container/services/grafana/jwt/jwks.json
===================================================================
--- /branches/amp_4_0/platform/tools/container/services/grafana/jwt/jwks.json	(nonexistent)
+++ /branches/amp_4_0/platform/tools/container/services/grafana/jwt/jwks.json	(working copy)
@@ -0,0 +1 @@
+{"keys":[]}
Index: /branches/amp_4_0/platform/tools/container/services/nginx/conf.d/app.conf
===================================================================
--- /branches/amp_4_0/platform/tools/container/services/nginx/conf.d/app.conf	(revision 2934)
+++ /branches/amp_4_0/platform/tools/container/services/nginx/conf.d/app.conf	(working copy)
@@ -6,6 +6,11 @@
     "~access_token=([^;]+)" $1;
 }
 
+map $http_cookie $grafana_jwt_token {
+    default "";
+    "~monitoring_access_token=([^;]+)" $1;
+}
+
 # --- HTTP Server Block (Redirects to HTTPS) ---
 server {
     listen 80;
@@ -57,11 +62,16 @@
         proxy_pass https://host.docker.internal:5601;
         proxy_http_version 1.1;
         proxy_set_header Host $host;
+        proxy_set_header X-Forwarded-Host $host;
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
         proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_set_header X-Forwarded-Prefix /visualization;
         proxy_set_header Authorization "Bearer $jwt_token";
+        proxy_set_header securitytenant "global_tenant";
         proxy_ssl_verify off;
+        proxy_redirect ~^(/app/.*)$ /visualization$1;
+        proxy_redirect ~^(/login.*)$ /visualization$1;
         proxy_set_header Upgrade $http_upgrade;
         proxy_set_header Connection "upgrade";
         proxy_cache_bypass $http_upgrade;
@@ -73,11 +83,16 @@
         proxy_pass https://host.docker.internal:5601;
         proxy_http_version 1.1;
         proxy_set_header Host $host;
+        proxy_set_header X-Forwarded-Host $host;
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
         proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_set_header X-Forwarded-Prefix /visualization;
         proxy_set_header Authorization "Bearer $jwt_token";
+        proxy_set_header securitytenant "global_tenant";
         proxy_ssl_verify off;
+        proxy_redirect ~^(/app/.*)$ /visualization$1;
+        proxy_redirect ~^(/login.*)$ /visualization$1;
         proxy_set_header Upgrade $http_upgrade;
         proxy_set_header Connection "upgrade";
         proxy_cache_bypass $http_upgrade;
@@ -89,11 +104,16 @@
         proxy_pass https://host.docker.internal:5601;
         proxy_http_version 1.1;
         proxy_set_header Host $host;
+        proxy_set_header X-Forwarded-Host $host;
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
         proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_set_header X-Forwarded-Prefix /visualization;
         proxy_set_header Authorization "Bearer $jwt_token";
+        proxy_set_header securitytenant "global_tenant";
         proxy_ssl_verify off;
+        proxy_redirect ~^(/app/.*)$ /visualization$1;
+        proxy_redirect ~^(/login.*)$ /visualization$1;
         proxy_set_header Upgrade $http_upgrade;
         proxy_set_header Connection "upgrade";
         proxy_cache_bypass $http_upgrade;
@@ -102,6 +122,23 @@
     }
 
     # Grafana at /monitoring/
+    location /monitoring {
+        if ($arg_access_token) {
+            add_header Set-Cookie "monitoring_access_token=$arg_access_token; Path=/monitoring/; HttpOnly";
+            return 302 /monitoring/;
+        }
+        proxy_pass http://host.docker.internal:3000;
+        proxy_http_version 1.1;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_set_header X-JWT-Assertion $grafana_jwt_token;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection "upgrade";
+        proxy_cache_bypass $http_upgrade;
+    }
+
     location /monitoring/ {
         # set $grafana "http://host.docker.internal:3000";
         proxy_pass http://host.docker.internal:3000; # Static pass allows system resolver to read /etc/hosts
@@ -110,6 +147,7 @@
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
         proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_set_header X-JWT-Assertion $grafana_jwt_token;
         proxy_set_header Upgrade $http_upgrade;
         proxy_set_header Connection "upgrade";
         proxy_cache_bypass $http_upgrade;
Index: /branches/amp_4_0/platform/tools/container/services/setup/configure_opensearch.sh
===================================================================
--- /branches/amp_4_0/platform/tools/container/services/setup/configure_opensearch.sh	(revision 2934)
+++ /branches/amp_4_0/platform/tools/container/services/setup/configure_opensearch.sh	(working copy)
@@ -36,7 +36,10 @@
 
 while [ $COUNT -lt $MAX_RETRIES ]; do
   # Capture output and exit code to debug failure
-  OUTPUT=$(curl -v -k -u "$ADMIN_USER:$ADMIN_PASS" "$OPENSEARCH_URL/_cluster/health" 2>&1)
+  OUTPUT=$(curl -v -k \
+    --cert /usr/share/opensearch/config/certs/admin.pem \
+    --key /usr/share/opensearch/config/certs/admin-key.pem \
+    "$OPENSEARCH_URL/_cluster/health" 2>&1)
   RET=$?
   
   if [ $RET -eq 0 ]; then
@@ -94,7 +97,11 @@
 log "Role mapping 'jwt_users' updated."
 
 log "Applying Index Template..."
-RESPONSE=$(curl -s -k -u "$ADMIN_USER:$ADMIN_PASS" -w "%{http_code}" -X PUT "$OPENSEARCH_URL/_template/acm_template" -H 'Content-Type: application/json' -d @/usr/share/opensearch/config/amplog_template.json)
+RESPONSE=$(curl -s -k \
+  --cert /usr/share/opensearch/config/certs/admin.pem \
+  --key /usr/share/opensearch/config/certs/admin-key.pem \
+  -w "%{http_code}" -X PUT "$OPENSEARCH_URL/_template/acm_template" \
+  -H 'Content-Type: application/json' -d @/usr/share/opensearch/config/amplog_template.json)
 log "Done. HTTP Response: $RESPONSE"
 
 MAX_RETRIES=60
@@ -102,7 +109,9 @@
 log "Waiting for OpenSearch Dashboards ($OPENSEARCH_DASHBOARDS_URL/visualization/api/status) (timeout: 300s)..."
 while [ $COUNT -lt $MAX_RETRIES ]; do
   # Capture HTTP Status and body - using -w to separate status code
-  HTTP_CODE=$(curl -s -k -o /tmp/dashboards_status.txt -w "%{http_code}" -u "$ADMIN_USER:$ADMIN_PASS" "$OPENSEARCH_DASHBOARDS_URL/visualization/api/status")
+  HTTP_CODE=$(curl -s -k -o /tmp/dashboards_status.txt -w "%{http_code}" -u "$ADMIN_USER:$ADMIN_PASS" \
+    -H "securitytenant: global_tenant" \
+    "$OPENSEARCH_DASHBOARDS_URL/visualization/api/status")
   STATUS_RES=$(cat /tmp/dashboards_status.txt)
 
   if [ "$HTTP_CODE" -eq 200 ] && echo "$STATUS_RES" | grep -q '"state":"green"'; then
@@ -177,7 +186,9 @@
 # Using -w %{http_code} to catch errors and -o to avoid dumping large JSON to logs
 # Set longer timeout (300s) for first-time import which can be slow
 log "Testing connectivity to $OPENSEARCH_DASHBOARDS_URL first..."
-curl -s -k -u "$ADMIN_USER:$ADMIN_PASS" -w "%{http_code}\n" -o /dev/null --connect-timeout 60 "$OPENSEARCH_DASHBOARDS_URL/visualization/api/status"
+curl -s -k -u "$ADMIN_USER:$ADMIN_PASS" -w "%{http_code}\n" -o /dev/null --connect-timeout 60 \
+  -H "securitytenant: global_tenant" \
+  "$OPENSEARCH_DASHBOARDS_URL/visualization/api/status"
 
 log "Starting import (this may take several minutes)..."
 HTTP_CODE=$(curl -v -k -u "$ADMIN_USER:$ADMIN_PASS" -o /tmp/import_res.json -w "%{http_code}" \
@@ -185,6 +196,7 @@
   --max-time 600 \
   -X POST "$OPENSEARCH_DASHBOARDS_URL/visualization/api/saved_objects/_import?overwrite=true" \
   -H "osd-xsrf: true" \
+  -H "securitytenant: global_tenant" \
   --form file=@/usr/share/opensearch/config/export.ndjson 2> /tmp/curl_debug.log)
 
 log "Import finished. HTTP Response: $HTTP_CODE"
@@ -198,6 +210,7 @@
   if [ -f /tmp/import_res.json ]; then
     cat /tmp/import_res.json
   fi
+  exit 1
 fi
 
 log "Configuration Complete!"
Index: /branches/amp_4_0/platform/tools/container/services/setup/setup.sh
===================================================================
--- /branches/amp_4_0/platform/tools/container/services/setup/setup.sh	(revision 2934)
+++ /branches/amp_4_0/platform/tools/container/services/setup/setup.sh	(working copy)
@@ -93,6 +93,9 @@
           challenge: true
         authentication_backend:
           type: intern
+    kibana:
+      # Land users in Global tenant by default so shared dashboards/index patterns are visible.
+      default_tenant: "global_tenant"
 EOF
 
 # --- 3. Internal Users Generation (internal_users.yml) ---
Index: /branches/amp_4_0/platform/tools/container/stack.yml
===================================================================
--- /branches/amp_4_0/platform/tools/container/stack.yml	(revision 2934)
+++ /branches/amp_4_0/platform/tools/container/stack.yml	(working copy)
@@ -27,6 +27,8 @@
     file: services/pgbouncer/userlist.txt
   grafana_datasources:
     file: services/grafana/provisioning/datasources/datasources.yaml
+  grafana_jwt_jwks:
+    file: services/grafana/jwt/jwks.json
   opensearch_config:
     file: services/opensearch/opensearch.yml
   haproxy_cfg:
@@ -101,6 +103,7 @@
     command: /usr/share/opensearch/opensearch-docker-entrypoint.sh opensearch
     secrets:
       - opensearch_initial_admin_password
+      - opensearch_jwt_secret
     volumes:
       - opensearch-data:/usr/share/opensearch/data
       - security-config-vol:/usr/share/opensearch/config/opensearch-security-mount:ro
@@ -344,11 +347,15 @@
       OPENSEARCH_SSL_VERIFICATIONMODE: certificate
       OPENSEARCH_USERNAME: admin
       OPENSEARCH_SSL_CERTIFICATEAUTHORITIES: '["/usr/share/opensearch-dashboards/config/certs/root-ca.pem"]'
+      OPENSEARCH_SECURITY_AUTH_TYPE: jwt
+      OPENSEARCH_SECURITY_JWT_HEADER: Authorization
+      OPENSEARCH_SECURITY_JWT_URL_PARAMETER: access_token
       SERVER_SSL_ENABLED: "true"
       SERVER_SSL_KEY: /usr/share/opensearch-dashboards/config/certs/node-key.pem
       SERVER_SSL_CERTIFICATE: /usr/share/opensearch-dashboards/config/certs/node.pem
       SERVER_BASEPATH: /visualization
       SERVER_REWRITEBASEPATH: "true"
+      SERVER_PUBLICBASEURL: https://${AMP_DOMAIN_OR_IP:-localhost}/visualization
     command: >
       bash -c "export OPENSEARCH_PASSWORD=\$$(cat /run/secrets/opensearch_initial_admin_password) && /usr/share/opensearch-dashboards/opensearch-dashboards-docker-entrypoint.sh opensearch-dashboards"
     secrets:
@@ -389,6 +396,11 @@
       - GF_AUTH_TOKEN_ROTATION_INTERVAL_MINUTES=1440
       - GF_AUTH_LOGIN_MAXIMUM_INACTIVE_LIFETIME_DAYS=30
       - GF_AUTH_LOGIN_MAXIMUM_LIFETIME_DAYS=30
+      - GF_AUTH_JWT_ENABLED=true
+      - GF_AUTH_JWT_HEADER_NAME=X-JWT-Assertion
+      - GF_AUTH_JWT_USERNAME_CLAIM=sub
+      - GF_AUTH_JWT_AUTO_SIGN_UP=true
+      - GF_AUTH_JWT_JWK_SET_FILE=/etc/grafana/jwt/jwks.json
       - GF_SECURITY_ADMIN_USER=admin
       - GF_SECURITY_ADMIN_PASSWORD=${GF_SECURITY_ADMIN_PASSWORD:-GArr@y2050}
       - GF_INSTALL_PLUGINS=grafana-opensearch-datasource
@@ -403,6 +415,8 @@
     configs:
       - source: grafana_datasources
         target: /etc/grafana/provisioning/datasources/datasources.yaml
+      - source: grafana_jwt_jwks
+        target: /etc/grafana/jwt/jwks.json
     volumes:
       # Removed local sqlite volume, data is now in Postgres
       # - grafana-data:/var/lib/grafana
Index: /branches/amp_4_0/platform/tools/container/stack.yml.template
===================================================================
--- /branches/amp_4_0/platform/tools/container/stack.yml.template	(revision 2934)
+++ /branches/amp_4_0/platform/tools/container/stack.yml.template	(working copy)
@@ -23,6 +23,8 @@
     file: services/pgbouncer/userlist.txt
   grafana_datasources:
     file: services/grafana/provisioning/datasources/datasources.yaml
+  grafana_jwt_jwks:
+    file: services/grafana/jwt/jwks.json
   opensearch_config:
     file: services/opensearch/opensearch.yml
   haproxy_cfg:
@@ -105,6 +107,7 @@
 
     secrets:
       - opensearch_initial_admin_password
+      - opensearch_jwt_secret
     volumes:
       - opensearch-data:/usr/share/opensearch/data
       - security-config-vol:/usr/share/opensearch/config/opensearch-security-mount:ro
@@ -289,11 +292,16 @@
       OPENSEARCH_SSL_VERIFICATIONMODE: certificate
       OPENSEARCH_USERNAME: admin
       OPENSEARCH_SSL_CERTIFICATEAUTHORITIES: '["/usr/share/opensearch-dashboards/config/certs/root-ca.pem"]'
+      OPENSEARCH_SECURITY_AUTH_TYPE: jwt
+      OPENSEARCH_SECURITY_JWT_HEADER: Authorization
+      OPENSEARCH_SECURITY_JWT_URL_PARAMETER: access_token
+      OPENSEARCH_SECURITY_MULTITENANCY_TENANTS_PREFERRED: '["Global","Private"]'
       SERVER_SSL_ENABLED: "true"
       SERVER_SSL_KEY: /usr/share/opensearch-dashboards/config/certs/node-key.pem
       SERVER_SSL_CERTIFICATE: /usr/share/opensearch-dashboards/config/certs/node.pem
       SERVER_BASEPATH: /visualization
       SERVER_REWRITEBASEPATH: "true"
+      SERVER_PUBLICBASEURL: https://${AMP_DOMAIN_OR_IP:-localhost}/visualization
     extra_hosts:
       - "host.docker.internal:host-gateway"
     configs:
@@ -346,6 +354,11 @@
       - GF_AUTH_TOKEN_ROTATION_INTERVAL_MINUTES=1440
       - GF_AUTH_LOGIN_MAXIMUM_INACTIVE_LIFETIME_DAYS=30
       - GF_AUTH_LOGIN_MAXIMUM_LIFETIME_DAYS=30
+      - GF_AUTH_JWT_ENABLED=true
+      - GF_AUTH_JWT_HEADER_NAME=X-JWT-Assertion
+      - GF_AUTH_JWT_USERNAME_CLAIM=sub
+      - GF_AUTH_JWT_AUTO_SIGN_UP=true
+      - GF_AUTH_JWT_JWK_SET_FILE=/etc/grafana/jwt/jwks.json
       - GF_SECURITY_ADMIN_USER=admin
       - GF_SECURITY_ADMIN_PASSWORD__FILE=/run/secrets/grafana_admin_password
       - GF_INSTALL_PLUGINS=grafana-opensearch-datasource
@@ -364,6 +377,8 @@
     configs:
       - source: grafana_datasources
         target: /etc/grafana/provisioning/datasources/datasources.yaml
+      - source: grafana_jwt_jwks
+        target: /etc/grafana/jwt/jwks.json
     volumes:
       - /dev/null:/dev/null # Placeholder to keep yaml valid if empty
     networks:
@@ -472,6 +487,7 @@
       - OPENSEARCH_PASSWORD_FILE=/run/secrets/opensearch_initial_admin_password
       - DJANGO_SETTINGS_MODULE=djproject.settings
       - PYTHONPATH=/ca/webui/htdocs/new/src
+      - AMP_SESSION_IDLE_TIMEOUT_MINUTES=${AMP_SESSION_IDLE_TIMEOUT_MINUTES:-15}
     volumes:
       # Mount configuration directories from host
       - type: bind
Index: /branches/amp_4_0/src/webui/webui/htdocs/new/src/djproject/urls.py
===================================================================
--- /branches/amp_4_0/src/webui/webui/htdocs/new/src/djproject/urls.py	(revision 2934)
+++ /branches/amp_4_0/src/webui/webui/htdocs/new/src/djproject/urls.py	(working copy)
@@ -26,7 +26,7 @@
 from hive.controller.backup_controller import handle_backup_req
 from hive.controller.restore_controller import handle_restore_req
 from hive.controller.utils import handle_observability_status_req, handle_observability_restart_req
-from hive.an_opensearch import opensearch_proxy, get_opensearch_sso_token
+from hive.an_opensearch import opensearch_proxy, get_opensearch_sso_token, get_grafana_sso_token
 from hive.controller.system_metrics import handle_get_latest_system_metrics, handle_get_historical_system_metrics
 from hive.controller.generic_controller import handle_service_query_req
 from hive.controller.notification_controller import handle_notification_req
@@ -98,6 +98,7 @@
     re_path(r'^observability-status$', handle_observability_status_req),
     re_path(r'^observability-restart$', handle_observability_restart_req),
     re_path(r'^opensearch-sso-token$', get_opensearch_sso_token),
+    re_path(r'^grafana-sso-token$', get_grafana_sso_token),
     re_path(r'^reporting/(?P<app>\w+)/(?P<filename>.*)$', reporting_downloading_handler),
     re_path(r'^reporting_logo$', reporting_logo_handler),
     re_path(r'^cm/save_setting$', save_setting),
Index: /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/observability/observability.html
===================================================================
--- /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/observability/observability.html	(revision 2934)
+++ /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/observability/observability.html	(working copy)
@@ -13,6 +13,11 @@
             <a (click)="openSearchDashboard()" class="a-link-color">Logs Dashboard <fa-icon
                     [icon]="['far', 'window-maximize']" size="sm"></fa-icon></a>
         </div>
+        <div class="dashboard-link-container">
+            <span>Visit Grafana Dashboard: </span>
+            <a (click)="openGrafanaDashboard()" class="a-link-color">Metrics Dashboard <fa-icon
+                    [icon]="['far', 'window-maximize']" size="sm"></fa-icon></a>
+        </div>
         <div class="table-container">
             <table mat-table [dataSource]="dataSource" class="an-table">
                 <ng-container matColumnDef="serial">
Index: /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/observability/observability.ts
===================================================================
--- /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/observability/observability.ts	(revision 2934)
+++ /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/observability/observability.ts	(working copy)
@@ -64,9 +64,7 @@
     this.system.getOpenSearchAuthToken().pipe(take(1)).subscribe({
       next: (result: any) => {
         if (result && result.token) {
-          const protocol = window.location.protocol;
-          const hostname = window.location.hostname;
-          const opensearchUrl = `${protocol}://${hostname}/visualization?access_token=${result?.token}`;
+          const opensearchUrl = `${window.location.origin}/visualization?access_token=${result?.token}`;
           window.open(opensearchUrl, '_blank');
         }
       },
@@ -77,6 +75,21 @@
     });
   }
 
+  openGrafanaDashboard() {
+    this.system.getGrafanaAuthToken().pipe(take(1)).subscribe({
+      next: (result: any) => {
+        if (result && result.token) {
+          const grafanaUrl = `${window.location.origin}/monitoring/?access_token=${result?.token}`;
+          window.open(grafanaUrl, '_blank');
+        }
+      },
+      error: error => {
+        this.ui.notifyError(`Error: ${error?.message}`);
+        console.log(error);
+      }
+    });
+  }
+
   restartService(element: any) {
     this.ui.confirmationService.openConfirmDialog({
       title: 'Restart Service',
Index: /branches/amp_4_0/src/webui/webui/htdocs/new/src/hive/an_opensearch.py
===================================================================
--- /branches/amp_4_0/src/webui/webui/htdocs/new/src/hive/an_opensearch.py	(revision 2934)
+++ /branches/amp_4_0/src/webui/webui/htdocs/new/src/hive/an_opensearch.py	(working copy)
@@ -16,6 +16,21 @@
 AUTH = ('admin', OPENSEARCH_PASSWORD)
 
 
+def _get_sso_token_ttl_minutes():
+    # Keep SSO token TTL aligned with AMP session timeout by default.
+    raw = os.environ.get('AMP_SSO_TOKEN_TTL_MINUTES') or os.environ.get('AMP_SESSION_IDLE_TIMEOUT_MINUTES') or '120'
+    try:
+        minutes = int(raw)
+    except (TypeError, ValueError):
+        minutes = 120
+
+    if minutes < 5:
+        minutes = 5
+    elif minutes > 1440:
+        minutes = 1440
+    return minutes
+
+
 def opensearch_proxy(request):
     client = OpenSearch(
         hosts=[{'host': HOST, 'port': PORT}],
@@ -29,7 +44,7 @@
     return HttpResponse(json.dumps(res), content_type='application/json')
 
 
-def get_opensearch_sso_token(request):
+def _generate_sso_token(request, default_roles='jwt_users', key_id=None):
     # Load the signing key from Docker secret
     JWT_SECRET_PATH = '/run/secrets/opensearch_jwt_secret'
     
@@ -42,11 +57,38 @@
 
     # Use timezone-aware UTC datetime objects
     now = datetime.datetime.now(datetime.timezone.utc)
+    token_ttl_minutes = _get_sso_token_ttl_minutes()
 
-    # ToDo: Implement RBAC
-    payload = {'sub': 'admin', 'exp': now + datetime.timedelta(hours=1), 'iat': now, }
+    # Use logged-in username when available; fallback to admin for legacy flows.
+    username = getattr(getattr(request, 'user', None), 'username', None) or 'admin'
 
-    # Generate token
-    token = jwt.encode(payload, secret_key, algorithm='HS256')
+    # JWT settings should mirror opensearch security config (issuer/roles/subject keys).
+    issuer = os.environ.get('OPENSEARCH_JWT_ISSUER', 'amp.com')
+    roles = os.environ.get('OPENSEARCH_JWT_ROLES', default_roles).split(',')
+    roles = [role.strip() for role in roles if role.strip()]
 
+    # ToDo: Implement RBAC for dynamic role assignment.
+    payload = {
+        'sub': username,
+        'iss': issuer,
+        'roles': roles,
+        'exp': now + datetime.timedelta(minutes=token_ttl_minutes),
+        'iat': now,
+    }
+
+    jwt_headers = {}
+    if key_id:
+        jwt_headers['kid'] = key_id
+
+    return jwt.encode(payload, secret_key, algorithm='HS256', headers=jwt_headers if jwt_headers else None)
+
+
+def get_opensearch_sso_token(request):
+    token = _generate_sso_token(request, default_roles='jwt_users')
     return JsonResponse({'token': token})
+
+
+def get_grafana_sso_token(request):
+    # Grafana SSO uses HS256 JWT validated against the mounted JWKS.
+    token = _generate_sso_token(request, default_roles='jwt_users', key_id='amp-grafana-hs256')
+    return JsonResponse({'token': token})
Index: /branches/amp_4_0/src/webui/webui/htdocs/new/src/hive/controller/utils.py
===================================================================
--- /branches/amp_4_0/src/webui/webui/htdocs/new/src/hive/controller/utils.py	(revision 2934)
+++ /branches/amp_4_0/src/webui/webui/htdocs/new/src/hive/controller/utils.py	(working copy)
@@ -10,6 +10,8 @@
     try:
         if request.method == 'GET':
             status_data = get_observability_services_status()
+            if isinstance(status_data, HttpResponse):
+                return status_data
             return JsonResponse(status_data, safe=False)
         else:
             return HttpResponse(json.dumps({
Index: /branches/amp_4_0/src/webui/webui/htdocs/new/src/hive/services/utils.py
===================================================================
--- /branches/amp_4_0/src/webui/webui/htdocs/new/src/hive/services/utils.py	(revision 2934)
+++ /branches/amp_4_0/src/webui/webui/htdocs/new/src/hive/services/utils.py	(working copy)
@@ -155,26 +155,25 @@
 
 
 def get_observability_services_status():
+    services_list = [
+        {'value': 'opensearch', 'label': 'Search & Analytics Engine'},
+        {'value': 'opensearch-dashboards', 'label': 'Logs Dashboards'},
+        {'value': 'logstash', 'label': 'Syslog Collector'},
+        {'value': 'telegraf', 'label': 'Metrics Collector'},
+    ]
     try:
-        services_list = [
-            {'value': 'opensearch', 'label': 'Search & Analytics Engine'},
-            {'value': 'opensearch-dashboards', 'label': 'Logs Dashboards'},
-            {'value': 'logstash', 'label': 'Syslog Collector'},
-            {'value': 'telegraf', 'label': 'Metrics Collector'},
-        ]
         result = []
         for service in services_list:
             is_running = check_service_health(service['value'])
             result.append({'value': is_running, 'label': service['label'], 'service': service['value']})
         return result
     except Exception as e:
-        oper_log('error', 'system', "Exception while fetching observability services status.")
-        message = str(e).replace("'", "")
-        message = 'Error while fetching observability services status, details: {}'.format(message)
-        return HttpResponse(json.dumps({
-            "message": "while fetching observability services status",
-            "details": "{}".format(message)
-        }), content_type="application/json", status=500)
+        oper_log('error', 'system', "Exception while fetching observability services status. details: {}".format(str(e)))
+        # Fallback to a deterministic payload to avoid bubbling 500 to the UI.
+        result = []
+        for service in services_list:
+            result.append({'value': False, 'label': service['label'], 'service': service['value']})
+        return result
 
 
 def perform_observability_services_restart(service_name):
Index: /branches/amp_4_0/src/webui/webui/htdocs/new/src/hive/session.py
===================================================================
--- /branches/amp_4_0/src/webui/webui/htdocs/new/src/hive/session.py	(revision 2934)
+++ /branches/amp_4_0/src/webui/webui/htdocs/new/src/hive/session.py	(working copy)
@@ -1,4 +1,5 @@
 import base64
+import os
 import time
 import uuid
 import traceback
@@ -22,6 +23,25 @@
 from hive.preference import *
 from hive.utils import HiveEnvironment, andebug, _thread_locals
 from . import auth
+
+
+def _get_session_idle_timeout_seconds():
+    # Configurable idle timeout with safe fallback.
+    raw = os.environ.get('AMP_SESSION_IDLE_TIMEOUT_MINUTES', '15')
+    try:
+        minutes = int(raw)
+    except (TypeError, ValueError):
+        minutes = 15
+
+    # Clamp to avoid accidental insecure/extreme values.
+    if minutes < 5:
+        minutes = 5
+    elif minutes > 1440:
+        minutes = 1440
+    return minutes * 60
+
+
+SESSION_IDLE_TIMEOUT_SECONDS = _get_session_idle_timeout_seconds()
 
 
 class ANSession(object):
@@ -488,7 +508,7 @@
                 else:
                     timestamp = sess.timestamp
                     difference = time.time() - timestamp
-                    if difference > 15 * 60 and timestamp != -1:
+                    if difference > SESSION_IDLE_TIMEOUT_SECONDS and timestamp != -1:
                         sess.logout()
                         _thread_locals.session = None
                         sess = None
