Build everything from source and run the full stack with security enabled (HTTPS, OBO token forwarding) across 4 terminal windows.
- Java 21 (e.g. Amazon Corretto)
- Node.js 22+ and Yarn 1
- Python 3.12+
uv(Python package manager:curl -LsSf https://astral.sh/uv/install.sh | sh)jq,curl,unzip- AWS credentials configured (for Bedrock LLM)
mkdir -p agent-quickstart && cd agent-quickstart
WORKSPACE=$(pwd)All repos will be cloned into this directory.
# Clone OpenSearch
git clone --depth 1 https://github.com/opensearch-project/OpenSearch.git
cd OpenSearch
# Build the min distribution (tar.gz without bundled JDK)
./gradlew :distribution:archives:no-jdk-darwin-tar:assemble -x test 2>&1 | tail -3
# Publish to local Maven (required for the security plugin build)
./gradlew publishToMavenLocal -x test -x javadoc --parallel 2>&1 | tail -3
cd $WORKSPACELinux users: Replace
no-jdk-darwin-tarwithno-jdk-linux-tar.
The security plugin must be built from source to match the OpenSearch version (e.g. 3.6.0-SNAPSHOT).
git clone --depth 1 https://github.com/opensearch-project/security.git
cd security
./gradlew assemble -x test -x integrationTest 2>&1 | tail -3
# Verify the zip was built
ls build/distributions/opensearch-security-*.zip
cd $WORKSPACEgit clone https://github.com/opensearch-project/ml-commons.git
cd ml-commons
# Build the plugin zip
OPENSEARCH_CORE_PATH=$WORKSPACE/OpenSearch \
./gradlew :opensearch-ml-plugin:bundlePlugin -x test -Pcrypto.standard=FIPS-140-3 2>&1 | tail -3
# Verify
ls plugin/build/distributions/opensearch-ml-*.zip
cd $WORKSPACEExtract the distribution and install plugins in dependency order.
OS_HOME=$WORKSPACE/opensearch-secure
# Extract the min distribution
mkdir -p $OS_HOME
tar xzf OpenSearch/distribution/archives/no-jdk-darwin-tar/build/distributions/opensearch-min-*.tar.gz \
--strip-components=1 -C $OS_HOME
# Copy bc-fips jar (required by security plugin, not included in min distribution)
BC_FIPS=$(find ~/.gradle/caches/modules-2 -name "bc-fips-*.jar" 2>/dev/null | head -1)
if [ -n "$BC_FIPS" ]; then
cp "$BC_FIPS" $OS_HOME/lib/
echo "Copied $(basename $BC_FIPS) to lib/"
fi
# 1) Install job-scheduler (dependency for ml-commons)
JS_ZIP=$(find ~/.gradle/caches/modules-2 -name "opensearch-job-scheduler-*.zip" 2>/dev/null | head -1)
if [ -n "$JS_ZIP" ]; then
$OS_HOME/bin/opensearch-plugin install --batch "file://$JS_ZIP"
fi
# 2) Install security plugin
SECURITY_ZIP=$(ls security/build/distributions/opensearch-security-*.zip | head -1)
$OS_HOME/bin/opensearch-plugin install --batch "file://$SECURITY_ZIP"
# 3) Install ml-commons (optional)
ML_ZIP=$(ls ml-commons/plugin/build/distributions/opensearch-ml-*.zip 2>/dev/null | head -1)
if [ -n "$ML_ZIP" ]; then
$OS_HOME/bin/opensearch-plugin install --batch "file://$ML_ZIP"
fi
# 4) Run security demo configuration (generates TLS certs + initial security index)
export OPENSEARCH_INITIAL_ADMIN_PASSWORD='MyStr0ngP@ss!'
bash $OS_HOME/plugins/opensearch-security/tools/install_demo_configuration.sh -y -i -s
# 5) Enable single-node discovery
echo "discovery.type: single-node" >> $OS_HOME/config/opensearch.ymlPlugin install order matters: job-scheduler must be installed before ml-commons (dependency). Security can be installed at any point before starting.
OBO tokens allow Dashboards to mint short-lived JWTs that carry the logged-in user's identity and permissions.
1. Edit the security config:
vi $OS_HOME/config/opensearch-security/config.ymlAdd the on_behalf_of block directly under the dynamic: key (same indentation level as http:, authc:, etc.):
config:
dynamic:
on_behalf_of:
enabled: true
signing_key: "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z"
encryption_key: "VGhpcyBpcyB0aGUgand0IGVuY3J5cHRpb24ga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z"
http:
# ... rest of existing configAlternatively, you can use sed to inject it (macOS):
# Copy clean config from security source
cp security/config/config.yml $OS_HOME/config/opensearch-security/config.yml
# Inject OBO config after the "dynamic:" line
sed -i '' '/^ dynamic:$/a\
\ on_behalf_of:\
\ enabled: true\
\ signing_key: "VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z"\
\ encryption_key: "VGhpcyBpcyB0aGUgand0IGVuY3J5cHRpb24ga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z"
' $OS_HOME/config/opensearch-security/config.ymlImportant: The REST API
PATCH /_plugins/_security/api/securityconfigreturns403 FORBIDDENfor config changes. You must usesecurityadmin.sh.
cd $WORKSPACE/opensearch-secure
OPENSEARCH_JAVA_HOME=$JAVA_HOME bin/opensearchWait for it to start, then verify in another terminal:
AUTH=$(echo -n 'admin:MyStr0ngP@ss!' | base64)
curl -sk -H "Authorization: Basic $AUTH" https://localhost:9200Note: The
@in the password breaks curl's-uflag. Always use base64-encoded Authorization headers.
After OpenSearch is running, apply the security config:
OS_HOME=$WORKSPACE/opensearch-secure
JAVA_HOME=$JAVA_HOME bash \
$OS_HOME/plugins/opensearch-security/tools/securityadmin.sh \
-f $OS_HOME/config/opensearch-security/config.yml \
-t config \
-icl -nhnv \
-key $OS_HOME/config/kirk-key.pem \
-cert $OS_HOME/config/kirk.pem \
-cacert $OS_HOME/config/root-ca.pemFlags: -f <file> -t config uploads the file as config type, -icl ignores cluster name, -nhnv skips hostname verification, -key/-cert/-cacert are admin TLS certs generated by demo config.
Verify OBO tokens work:
AUTH=$(echo -n 'admin:MyStr0ngP@ss!' | base64)
curl -sk -H "Authorization: Basic $AUTH" -X POST \
'https://localhost:9200/_plugins/_security/api/obo/token' \
-H 'Content-Type: application/json' \
-d '{"description": "test"}'You should see a response containing authenticationToken.
If you see cluster create-index blocked or flood-stage watermark errors:
AUTH=$(echo -n 'admin:MyStr0ngP@ss!' | base64)
curl -sk -H "Authorization: Basic $AUTH" -X PUT \
'https://localhost:9200/_cluster/settings' \
-H 'Content-Type: application/json' \
-d '{"persistent":{"cluster.routing.allocation.disk.watermark.flood_stage":"99%","cluster.routing.allocation.disk.watermark.high":"98%","cluster.routing.allocation.disk.watermark.low":"97%"}}'
curl -sk -H "Authorization: Basic $AUTH" -X PUT \
'https://localhost:9200/.kibana_1/_settings' \
-H 'Content-Type: application/json' \
-d '{"index.blocks.read_only_allow_delete":null}'The MCP server must use header-based auth (OPENSEARCH_HEADER_AUTH=true) so it forwards the OBO Bearer token to OpenSearch instead of using basic auth.
export OPENSEARCH_URL="https://localhost:9200"
export OPENSEARCH_HEADER_AUTH=true
export OPENSEARCH_SSL_VERIFY=false
# Do NOT set OPENSEARCH_USERNAME or OPENSEARCH_PASSWORD
uv tool run opensearch-mcp-server-py --transport stream --port 3030Verify: you should see Uvicorn running on http://0.0.0.0:3030 in the output.
When requests come in, the logs should show [HEADER AUTH] Using Authorization Bearer header (not [BASIC AUTH]).
cd opensearch-agent-server
source .venv/bin/activate
python run_server.pyThe agent server reads .env for configuration:
# .env
MCP_SERVER_URL=http://localhost:3030/mcp
AG_UI_AUTH_ENABLED=false
AG_UI_CORS_ORIGINS=http://localhost:5601Verify: http://localhost:8001/health should return OK.
cd $WORKSPACE
git clone --depth 1 https://github.com/opensearch-project/OpenSearch-Dashboards.git
cd OpenSearch-Dashboards
yarn osd bootstrapEdit config/opensearch_dashboards.yml — set these values (uncomment or add):
# Connect to secured OpenSearch
opensearch.hosts: ["https://localhost:9200"]
opensearch.username: "admin"
opensearch.password: "MyStr0ngP@ss!"
opensearch.ssl.verificationMode: none
# Enable new home page UI (required for chat button)
uiSettings:
overrides:
"home:useNewHomePage": true
# Enable chat with AG-UI agent server + OBO token forwarding
chat:
enabled: true
agUiUrl: "http://localhost:8001/runs"
forwardCredentials: true
# Enable context provider (sends page context to agent)
contextProvider:
enabled: trueyarn start --no-base-pathWait for bundles compiled successfully in the output (takes a few minutes on first run).
Navigate to http://localhost:5601 and log in:
- Username:
admin - Password:
MyStr0ngP@ss!
The chat button should appear in the top header. Send a message to test the full flow.
Start components in this order (each must be ready before the next):
- OpenSearch — wait for
https://localhost:9200to respond - MCP Server — wait for
Uvicorn running on http://0.0.0.0:3030 - Agent Server — wait for
Starting OpenSearch Agent Server on 0.0.0.0:8001 - Dashboards — wait for
bundles compiled successfully
Browser
-> http://localhost:5601 (Dashboards)
-> POST /api/chat/proxy (Dashboards server-side)
1. Mint OBO token: POST https://localhost:9200/_plugins/_security/api/obo/token
2. Forward to Agent Server with Authorization: Bearer <obo_token>
-> http://localhost:8001/runs (Agent Server)
-> http://localhost:3030/mcp (MCP Server, OPENSEARCH_HEADER_AUTH=true)
-> https://localhost:9200 (OpenSearch, validates Bearer JWT)
With forwardCredentials: true, Dashboards generates a short-lived OBO JWT (5 min TTL) for the logged-in user and sends it as a Bearer token through the entire chain. OpenSearch validates the JWT signature, extracts the user identity (sub) and encrypted roles, and enforces that user's permissions.
Create users with different permission levels to test role-based access via OBO:
AUTH=$(echo -n 'admin:MyStr0ngP@ss!' | base64)
# Create analyst (read-only)
curl -sk -H "Authorization: Basic $AUTH" -X PUT \
'https://localhost:9200/_plugins/_security/api/internalusers/analyst' \
-H 'Content-Type: application/json' \
-d '{"password":"R3adOnly@sec","backend_roles":["readall"],"description":"Read-only analyst user"}'
# Create developer (read + write)
curl -sk -H "Authorization: Basic $AUTH" -X PUT \
'https://localhost:9200/_plugins/_security/api/internalusers/developer' \
-H 'Content-Type: application/json' \
-d '{"password":"Dev1@pass123","backend_roles":["readall"],"description":"Developer user"}'# Analyst role: read-only + cluster monitor + OBO token generation
curl -sk -H "Authorization: Basic $AUTH" -X PUT \
'https://localhost:9200/_plugins/_security/api/roles/analyst_role' \
-H 'Content-Type: application/json' \
-d '{
"cluster_permissions": ["cluster_monitor","security:obo/create"],
"index_permissions": [{"index_patterns":["*"],"allowed_actions":["read","indices_monitor","get"]}]
}'
# Developer role: read + write + manage indices + OBO
curl -sk -H "Authorization: Basic $AUTH" -X PUT \
'https://localhost:9200/_plugins/_security/api/roles/developer_role' \
-H 'Content-Type: application/json' \
-d '{
"cluster_permissions": ["cluster_monitor","cluster_manage_index_templates","security:obo/create"],
"index_permissions": [{"index_patterns":["*"],"allowed_actions":["crud","create_index","manage","indices_monitor"]}]
}'Note: The
security:obo/createpermission is required for non-admin users to generate OBO tokens.
# Map to custom roles
curl -sk -H "Authorization: Basic $AUTH" -X PUT \
'https://localhost:9200/_plugins/_security/api/rolesmapping/analyst_role' \
-H 'Content-Type: application/json' -d '{"users":["analyst"]}'
curl -sk -H "Authorization: Basic $AUTH" -X PUT \
'https://localhost:9200/_plugins/_security/api/rolesmapping/developer_role' \
-H 'Content-Type: application/json' -d '{"users":["developer"]}'
# Map to built-in roles for index read + Dashboards access
curl -sk -H "Authorization: Basic $AUTH" -X PUT \
'https://localhost:9200/_plugins/_security/api/rolesmapping/readall' \
-H 'Content-Type: application/json' -d '{"users":["analyst","developer"]}'
curl -sk -H "Authorization: Basic $AUTH" -X PUT \
'https://localhost:9200/_plugins/_security/api/rolesmapping/kibana_user' \
-H 'Content-Type: application/json' -d '{"users":["analyst","developer"]}'| User | Password | Can Read | Can Write | OBO Token |
|---|---|---|---|---|
admin |
MyStr0ngP@ss! |
Yes | Yes | Yes |
analyst |
R3adOnly@sec |
Yes | No | Yes |
developer |
Dev1@pass123 |
Yes | Yes | Yes |
To test: log in as analyst in Dashboards, open chat, and ask the agent to create an index. It should fail with a permissions error. Log in as developer and the same request should succeed.
Note: The agent server caches agents per name. Restart the agent server between user switches to ensure the new user's OBO token is used.
| Key | Value |
|---|---|
| Admin password | MyStr0ngP@ss! |
| OBO signing key (base64) | VGhpcyBpcyB0aGUgand0IHNpZ25pbmcga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z |
| OBO signing key (decoded) | This is the jwt signing key for an on behalf of token authentication backend for testing of extensions |
| OBO encryption key (base64) | VGhpcyBpcyB0aGUgand0IGVuY3J5cHRpb24ga2V5IGZvciBhbiBvbiBiZWhhbGYgb2YgdG9rZW4gYXV0aGVudGljYXRpb24gYmFja2VuZCBmb3IgdGVzdGluZyBvZiBleHRlbnNpb25z |
# Stop OpenSearch (if running in daemon mode)
kill $(cat $WORKSPACE/opensearch-secure/opensearch.pid)
# Stop MCP Server, Agent Server, Dashboards
# Ctrl+C in each terminal| Problem | Solution |
|---|---|
Unauthorized with curl |
The @ in the password breaks curl's -u flag. Use base64 auth header instead (see examples above). |
cluster create-index blocked |
Disk watermark triggered. Clear with the cluster settings commands in the OpenSearch section. |
registerPPLValidationProvider is not a function |
Stale build cache. Run yarn osd bootstrap then restart Dashboards. |
MCP client session is not running on 2nd chat |
Restart the agent server. Fixed by the mcp_client.start() change in default_agent.py. |
fetch failed on chat |
Agent server not running on port 8001. |
Failed to start MCP client |
MCP server not running on port 3030. |
[BASIC AUTH] in MCP logs |
MCP server using basic auth instead of OBO token. Restart with OPENSEARCH_HEADER_AUTH=true and unset OPENSEARCH_USERNAME/OPENSEARCH_PASSWORD. |
Password is similar to user name |
Security plugin rejects passwords that resemble the username. Use a different password. |
no permissions for [security:obo/create] |
User's role is missing security:obo/create cluster permission. Add it to the role. |
Security plugin NoClassDefFoundError: BouncyCastleFipsProvider |
Copy bc-fips-*.jar from ~/.gradle/caches/modules-2/ into $OS_HOME/lib/. |
Missing plugin [opensearch-job-scheduler] when installing ml-commons |
Install job-scheduler before ml-commons. |
Security REST API returns 403 FORBIDDEN for config changes |
Use securityadmin.sh instead of the REST API for security config modifications. |