
Senhasegura
- 47 installs
- 6 repo stars
- Updated July 22, 2026
- julianobarbosa/claude-code-skills
Senhasegura PAM platform integration - A2A OAuth 2.0, PAM Core credentials, SSH key rotation, DSM CLI for CI/CD, External Secrets Operator, MySafe.
About
Senhasegura PAM platform integration - A2A OAuth 2.0, credentials, SSH key rotation, DSM CLI, External Secrets Operator, MySafe, MCP server.. Use for senhasegura setup, credential management.
- intermediate skill
- core: security
Senhasegura by the numbers
- 47 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #1,355 of 2,203 Security skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/julianobarbosa/claude-code-skills --skill senhaseguraAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 47 |
|---|---|
| repo stars | ★ 6 |
| Last updated | July 22, 2026 |
| Repository | julianobarbosa/claude-code-skills ↗ |
What it does
Senhasegura PAM platform integration - A2A OAuth 2.0, PAM Core credentials, SSH key rotation, DSM CLI for CI/CD, External Secrets Operator, MySafe.
Files
Senhasegura
Skill Type: Library/API Reference (Type 1) + Data Fetching (Type 3).
Practical integration guide for the senhasegura (Segura) Privileged Access Management platform. Covers A2A OAuth 2.0 auth, PAM Core credential and SSH key APIs, DevOps Secret Manager (DSM) for CI/CD, External Secrets Operator for Kubernetes, MySafe, and an opt-in MCP server.
Voice Notification (on invocation)
curl -s -X POST http://localhost:31337/notify \
-H "Content-Type: application/json" \
-d '{"message": "Running the Senhasegura skill"}' \
> /dev/null 2>&1 &Workflow Routing
| Trigger | Workflow |
|---|---|
| "set up A2A", "create senhasegura application", first-time OAuth client | Workflows/SetupA2A.md |
| "sync secrets to Kubernetes", ESO + senhasegura DSM | Workflows/SyncKubernetesSecrets.md |
| "rotate password", schedule rotation, Executions module | Workflows/RotatePasswords.md |
"fetch secrets in pipeline", DSM runb in CI/CD | Workflows/InjectCiCdSecrets.md |
| "register SSH key", manage SSH key rotation | Workflows/RegisterSshKey.md |
Reference Routing
| Topic | File |
|---|---|
| OAuth 2.0 flow, A2A application creation, token caching | Authentication.md |
| PAM Core endpoints — credentials, custody, SSH keys | PamApi.md |
| DSM CLI, runb, mapping.json, External Secrets Operator | Dsm.md |
| MySafe, Python/TypeScript clients, CI/CD pipeline templates | Integrations.md |
| Errors, debug mode, rate limits, common failure modes | Troubleshooting.md |
| MCP server (opt-in Claude Code integration) | References/McpIntegration.md + Tools/SenhaseguraMcpServer.ts |
| Legacy OAuth 1.0 reference | References/OAuth1Legacy.md |
| Python SDK reference (PAI standard is TypeScript+Bun) | References/PythonSdk.md |
Gotchas
These are non-obvious senhasegura behaviors that bite users. Add new entries as they're discovered.
| # | Gotcha |
|---|---|
| 1 | GET /iso/coe/senha?credentialId=N auto-locks the credential into custody. You MUST DELETE /iso/pam/credential/custody/N after, or the credential is held until manual release. Concurrent jobs deadlock on the second fetch. |
| 2 | OAuth 2.0 access token TTL is 3600s. Cache and refresh at expiry minus 60s buffer, not on 401 retry. 401 retry storms hammer the IDP and trigger rate limits. |
| 3 | /iso/coe/senha is the legacy A2A v1 password retrieval path; /api/pam/credential/{id} returns metadata only. The legacy path is current — do not refactor it away. |
| 4 | A2A authorization IP restriction is matched against the HTTP source IP as seen by senhasegura — for Kubernetes, that's the egress NAT, not the pod IP. Whitelist the cluster egress. ESO failures surface as a generic "could not get provider client". |
| 5 | DSM runb writes secrets to .runb.vars in the current working directory. If cwd is the repo root, it can be accidentally committed. Always run from a tmpdir or set SENHASEGURA_SECRETS_FILE=/tmp/runb.$$.vars. |
| 6 | External Secrets Operator provider key is senhasegura (lowercase) and module is DSM (uppercase). Mixed case = silent provider-not-found; ESO just fails to reconcile with no clear error. |
| 7 | dsm runb --tool-name <X> accepts github, azure-devops, gitlab, linux. Using linux outside Linux containers (Windows runners, unspecified) prevents masking of secret values in CI logs — secrets leak into log output. |
| 8 | The Python and TypeScript clients in References/ do not auto-release custody on raw getPassword. Use the provided withPassword(...) / password_context(...) helpers, which wrap try/finally. |
| 9 | The token endpoint is /iso/oauth2/token — note /iso/, not /api/. Senhasegura's path scheme mixes /iso/ (legacy/console) and /api/ (newer REST) inconsistently. |
| 10 | Most enterprise installs use internal CAs. Reach for ignoreSslCertificate: true / SENHASEGURA_INSECURE: true only for local debugging. The proper path is mounting the CA bundle into ESO via the ca field. |
Quick Reference
# 1. Get token (1h TTL)
TOKEN=$(curl -s -X POST "$SENHASEGURA_URL/iso/oauth2/token" \
-d "grant_type=client_credentials" \
-d "client_id=$SENHASEGURA_CLIENT_ID" \
-d "client_secret=$SENHASEGURA_CLIENT_SECRET" | jq -r '.access_token')
# 2. List credentials
curl -H "Authorization: Bearer $TOKEN" "$SENHASEGURA_URL/api/pam/credential"
# 3. Get password (auto-locks custody — see Gotcha #1)
curl -H "Authorization: Bearer $TOKEN" \
"$SENHASEGURA_URL/iso/coe/senha?credentialId=123"
# 4. Release custody (REQUIRED after step 3)
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
"$SENHASEGURA_URL/iso/pam/credential/custody/123"
# 5. DSM secret fetch via CLI
dsm runb --tool-name github \
--application my-app --system production --environment prod
source .runb.vars && rm -f .runb.varsEnvironment Variables
SENHASEGURA_URL="https://senhasegura.example.com"
SENHASEGURA_CLIENT_ID="oauth2-client-id"
SENHASEGURA_CLIENT_SECRET="oauth2-client-secret"
# DSM CLI optionals
SENHASEGURA_CONFIG_FILE="/path/to/config.yaml"
SENHASEGURA_MAPPING_FILE="/path/to/mapping.json"
SENHASEGURA_SECRETS_FILE="/tmp/runb.$$.vars" # don't write into repo root
SENHASEGURA_TIMEOUT="30"
# SENHASEGURA_INSECURE — local debug only; mount the CA bundle in prodDocumentation Links
- Official documentation
- A2A module
- DSM module
- PAM Core API reference
- DSM CLI on GitHub
- External Secrets Operator — senhasegura provider
Execution Log
Append after every workflow run:
echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Senhasegura","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' \
>> ~/.claude/PAI/MEMORY/SKILLS/execution.jsonlAuthentication
Senhasegura supports OAuth 2.0 (recommended), OAuth 1.0 (legacy — see References/OAuth1Legacy.md), and AWS Signature for AWS workloads.
OAuth 2.0 (recommended)
Token endpoint
POST {SENHASEGURA_URL}/iso/oauth2/token — note /iso/, not /api/ (SKILL.md Gotcha #9).
Request
curl -X POST "$SENHASEGURA_URL/iso/oauth2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=$SENHASEGURA_CLIENT_ID" \
-d "client_secret=$SENHASEGURA_CLIENT_SECRET"Response
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600
}Using the token
curl -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"$SENHASEGURA_URL/api/pam/credential"Token caching pattern (correct)
Cache the token in memory and refresh at expiry minus 60 seconds, not on 401. The TypeScript and Python clients in References/ implement this; copy that pattern when writing your own.
this.tokenExpiry = new Date(Date.now() + data.expires_in * 1000 - 60_000);self.token_expiry = datetime.now() + timedelta(seconds=data["expires_in"] - 60)A2A application creation
Walk through Workflows/SetupA2A.md for the console steps. Summary:
1. A2A → Applications → New — create the app, set OAuth 2.0, Enabled. 2. A2A → Authorizations → New — bind the app to a module (PAM Core, DSM, etc.) with permission, IP restriction, and optional credential filter. 3. Copy client_id and client_secret from the application's Authorization view.
One A2A authorization per application. Don't share credentials across services — least privilege and audit clarity both depend on it.
IP restriction — what senhasegura actually sees
The IP restriction matches the source IP from the HTTP request as seen by senhasegura. In practice:
| Caller | What senhasegura sees |
|---|---|
| Bare bash on a VM | The VM's public/egress IP |
| Pod in Kubernetes | The cluster's egress NAT (NOT the pod IP) |
| GitHub Actions | The runner's egress (broad GitHub IP ranges) |
| Self-hosted runner / on-prem | Your egress firewall |
Whitelist the egress, not the source. Failures from this surface as "could not get provider client" in ESO logs and as IP not allowed in direct API calls.
AWS Signature
Used when calling senhasegura from AWS workloads with IAM-based identity. Configure in the A2A authorization dialog. Most users should pick OAuth 2.0 unless they have a specific AWS-side requirement.
OAuth 1.0 (legacy)
Still supported, no longer recommended for new integrations. See References/OAuth1Legacy.md if you must.
DevOps Secret Manager (DSM)
DSM is senhasegura's secret store for DevOps workflows. Two main consumers: the dsm CLI (for CI/CD pipelines) and the External Secrets Operator (for Kubernetes).
DSM CLI
Install
curl -LO https://github.com/senhasegura/dsmcli/releases/latest/download/dsm-linux-amd64
chmod +x dsm-linux-amd64
sudo mv dsm-linux-amd64 /usr/local/bin/dsm
dsm --versionConfiguration
Create ~/.senhasegura/config.yaml (chmod 600):
SENHASEGURA_URL: "https://senhasegura.example.com"
SENHASEGURA_CLIENT_ID: "your-client-id"
SENHASEGURA_CLIENT_SECRET: "your-client-secret"
# Optional
SENHASEGURA_MAPPING_FILE: "/path/to/mapping.json"
SENHASEGURA_SECRETS_FILE: "/tmp/runb.$$.vars"
SENHASEGURA_DISABLE_RUNB: 0A full template is at References/DsmConfigExample.yaml.
Or use environment variables (preferred for CI):
export SENHASEGURA_URL="https://senhasegura.example.com"
export SENHASEGURA_CLIENT_ID="your-client-id"
export SENHASEGURA_CLIENT_SECRET="your-client-secret"
export SENHASEGURA_CONFIG_FILE="/path/to/config.yaml" # optionaldsm runb — fetch secrets at runtime
dsm runb \
--tool-name github \
--application my-app \
--system production \
--environment prod
source .runb.vars
# ... use secrets ...
rm -f .runb.vars # always, even on failure--tool-name controls log masking and must match the actual runner: github, azure-devops, gitlab, linux. See SKILL.md Gotcha #7.
Run from a tmpdir, not the repo root, so .runb.vars cannot be committed (Gotcha #5).
Mapping file (write-back)
For pipelines that register or update secrets, configure a mapping file:
{
"access_keys": [
{
"name": "AWS_PROD_KEYS",
"type": "aws",
"fields": {
"access_key_id": "AWS_ACCESS_KEY_ID",
"secret_access_key": "AWS_SECRET_ACCESS_KEY"
}
}
],
"credentials": [
{
"name": "DATABASE_CREDS",
"fields": {
"user": "DB_USER",
"password": "DB_PASSWORD",
"host": "DB_HOST"
}
}
],
"key_value": [
{
"name": "API_TOKENS",
"fields": ["API_KEY", "API_SECRET", "WEBHOOK_SECRET"]
}
]
}Set SENHASEGURA_MAPPING_FILE=/path/to/mapping.json. Full template: References/MappingExample.json.
DSM API
| Method | Endpoint | Purpose |
|---|---|---|
GET | /api/dsm/secret | List secrets |
GET | /api/dsm/secret/{identifier} | Get a secret by identifier |
POST | /api/dsm/secret | Create a secret |
PUT | /api/dsm/secret/{identifier} | Update a secret |
DELETE | /api/dsm/secret/{identifier} | Delete a secret |
The Python DSMClient and TypeScript clients in References/ wrap these.
Kubernetes — External Secrets Operator
ESO is the recommended way to project DSM secrets into Kubernetes Secret objects. Full step-by-step: Workflows/SyncKubernetesSecrets.md.
Provider casing — critical
senhasegura (lowercase) and DSM (uppercase). Mixed case = silent provider-not-found (SKILL.md Gotcha #6).
SecretStore (single namespace)
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: senhasegura-dsm
namespace: default
spec:
provider:
senhasegura:
url: "https://senhasegura.example.com"
module: DSM
auth:
clientId:
secretRef:
name: senhasegura-auth
key: clientId
namespace: external-secrets
clientSecretSecretRef:
name: senhasegura-auth
key: clientSecret
namespace: external-secretsFull template: References/SecretStoreExample.yaml.
ClusterSecretStore (multi-namespace)
Identical spec, kind: ClusterSecretStore, no metadata.namespace. Bind RBAC tightly — anything in the cluster can create an ExternalSecret against it.
ExternalSecret patterns
Explicit keys:
spec:
refreshInterval: 1h
secretStoreRef:
name: senhasegura-dsm
kind: SecretStore
target:
name: db-secret
creationPolicy: Owner
data:
- secretKey: username
remoteRef:
key: database-prod
property: username
- secretKey: password
remoteRef:
key: database-prod
property: passwordExtract all fields:
spec:
refreshInterval: 30m
secretStoreRef:
name: senhasegura-dsm
kind: SecretStore
target:
name: api-config
creationPolicy: Owner
dataFrom:
- extract:
key: api-settings-prodFull template: References/ExternalSecretExample.yaml.
TLS
If your senhasegura uses an internal CA, mount the CA bundle into the ESO pod and reference it via the ca field. Don't use ignoreSslCertificate: true outside local debugging (SKILL.md Gotcha #10).
RBAC for consuming pods
Restrict consumers to specific secret names:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: secret-reader
namespace: my-app
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["db-secret", "api-secret"]
verbs: ["get"]Integrations
Where senhasegura plugs into the rest of your stack: language clients, CI/CD pipelines, and the MySafe end-user surface.
TypeScript / Bun client (PAI standard)
Drop-in client at References/TypescriptClient.ts. Run with Bun:
bun run References/TypescriptClient.tsHighlights:
authenticate()caches the token and refreshes at expiry minus 60swithPassword(credentialId, callback)retrieves the password and always releases custody infinally— use this instead of rawgetPassword- Strongly typed
Credential,CredentialPassword, andApiResponse<T>shapes
const client = new SenhaseguraClient({
baseUrl: process.env.SENHASEGURA_URL!,
clientId: process.env.SENHASEGURA_CLIENT_ID!,
clientSecret: process.env.SENHASEGURA_CLIENT_SECRET!,
});
const credentials = await client.listCredentials();
await client.withPassword("123", async (password) => {
// password is valid here
});
// Custody released automaticallyPython client
References/PythonClient.py — full SenhaseguraClient and DSMClient. Key pattern:
client = SenhaseguraClient() # reads SENHASEGURA_* env vars
with client.password_context("123") as password:
# use password
pass
# Custody released automaticallyFor broader Python guidance (including a discussion of the senhasegura PyPI package and OAuth 1.0 fallback), see References/PythonSdk.md.
CI/CD pipelines
End-to-end workflow: Workflows/InjectCiCdSecrets.md.
Per-platform templates:
| Platform | File |
|---|---|
| GitHub Actions | References/GithubActionsExample.yaml |
| Azure DevOps | References/AzurePipelinesExample.yaml |
| GitLab CI | References/GitlabCiExample.yaml |
The same shape repeats across all three:
1. Install DSM CLI in the runner 2. Export SENHASEGURA_* env vars from the platform's secret store 3. dsm runb --tool-name <github|azure-devops|gitlab> ... from a tmpdir 4. source .runb.vars 5. Use secrets 6. Cleanup rm -f .runb.vars in an always-run step
MySafe
End-user vault for personal/team credentials, separate from machine A2A flows.
Web access: https://senhasegura.example.com/mysafe
Features:
- Passwords — store and share login credentials
- Notes — secure text notes
- Files — encrypted file storage
- API Secrets — API keys, tokens, client credentials
Browser extension
Chrome — Segura MySafe Extension. Auto-fill, quick credential creation, vault search.
Sharing
| Mode | Use case | Notes |
|---|---|---|
| Internal | Share with MySafe users / groups | Permission: view or edit |
| External (link) | Share with non-users | Set expiration, view limit, can revoke anytime |
External-link sharing is the right tool when you need to send a one-off password to someone outside the org. Don't paste credentials into chat.
MCP server (Claude Code)
Opt-in: a runnable MCP server at Tools/SenhaseguraMcpServer.ts exposes core PAM operations as Claude Code tools.
See References/McpIntegration.md for the configuration and full tool catalog.
SCIM provisioning
Senhasegura supports SCIM for user/group sync from an IdP (Azure AD, Okta, Google). Configure under Settings → SCIM in the console; the IdP-side configuration varies. Out of scope for this skill — see vendor docs.
PAM Core API
Endpoints for credentials, password retrieval, custody management, and SSH keys.
Credentials
| Method | Endpoint | Purpose |
|---|---|---|
GET | /api/pam/credential | List all credentials this authorization can see |
GET | /api/pam/credential/{id} | Get credential metadata (no password) |
POST | /api/pam/credential | Create a credential |
PUT | /api/pam/credential/{id} | Update a credential |
DELETE | /api/pam/credential/{id} | Disable a credential |
GET | /iso/coe/senha?credentialId={id} | Retrieve password (LEGACY path, current use) — auto-locks custody |
DELETE | /iso/pam/credential/custody/{id} | Release custody after password retrieval |
POST | /api/pam/credential/{id}/rotate | Trigger immediate rotation |
List credentials
curl -H "Authorization: Bearer $TOKEN" "$SENHASEGURA_URL/api/pam/credential"{
"response": {
"status": 200,
"credentials": [
{
"id": "123",
"identifier": "db-admin-prod",
"username": "admin",
"hostname": "db.example.com",
"ip": "10.0.1.50",
"type": "Local User"
}
]
}
}Retrieve password (custody lifecycle)
The two-step lifecycle is mandatory:
# 1. Fetch — auto-locks the credential into custody (SKILL.md Gotcha #1)
curl -H "Authorization: Bearer $TOKEN" \
"$SENHASEGURA_URL/iso/coe/senha?credentialId=123"
# 2. Release custody — REQUIRED, even on error
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
"$SENHASEGURA_URL/iso/pam/credential/custody/123"Always wrap retrieval + release in try/finally in any client. The TypeScript and Python clients in References/ provide withPassword(...) and password_context(...) helpers for this. Use them. Don't roll your own without the finally block (Gotcha #8).
Create credential
curl -X POST "$SENHASEGURA_URL/api/pam/credential" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"identifier": "new-service-account",
"username": "svc_app",
"password": "InitialP@ss123",
"hostname": "app-server.example.com",
"ip": "10.0.2.100",
"type": "Local User",
"additional_info": "Service account for app",
"tags": ["production", "critical"]
}'After creation, prefer enabling automatic rotation immediately so the bootstrap password isn't long-lived.
SSH Keys
| Method | Endpoint | Purpose |
|---|---|---|
GET | /api/pam/sshkey | List all SSH keys |
GET | /api/pam/sshkey/{id} | Get key (private key included) |
POST | /api/pam/sshkey | Register a new SSH key |
PUT | /api/pam/sshkey/{id} | Update key metadata |
POST | /api/pam/sshkey/{id}/rotate | Trigger key rotation |
See Workflows/RegisterSshKey.md for the end-to-end flow.
Register key
curl -X POST "$SENHASEGURA_URL/api/pam/sshkey" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"identifier": "deploy-key-prod",
"username": "deploy",
"hostname": "*.prod.example.com",
"public_key": "ssh-ed25519 AAAAC3Nza...",
"private_key": "-----BEGIN OPENSSH PRIVATE KEY-----...",
"passphrase": "optional-passphrase",
"auto_rotate": true,
"rotation_days": 90
}'Trigger rotation
curl -X POST "$SENHASEGURA_URL/api/pam/sshkey/456/rotate" \
-H "Authorization: Bearer $TOKEN"Response codes
| Code | Meaning | Action |
|---|---|---|
200 | Success | Process the response |
400 | Bad request | Check JSON shape and required fields |
401 | Unauthorized | Token expired or invalid — re-authenticate |
403 | Forbidden | Authorization doesn't grant access — check A2A permissions and credential filter |
404 | Not found | Wrong ID, or credential not visible to this authorization |
429 | Rate limited | Implement exponential backoff with jitter |
5xx | Server error | Retry with backoff; persistent → contact senhasegura support |
# Azure DevOps Pipeline with Senhasegura DSM Integration
# Fetches secrets from Senhasegura and injects them into the pipeline
trigger:
branches:
include:
- main
- develop
pr:
branches:
include:
- main
pool:
vmImage: 'ubuntu-latest'
variables:
- group: senhasegura-credentials # Variable group containing SENHASEGURA_* vars
- name: APPLICATION_NAME
value: 'my-application'
- name: SYSTEM_NAME
value: 'production'
- name: ENVIRONMENT
value: 'prod'
stages:
- stage: Build
displayName: 'Build and Test'
jobs:
- job: BuildJob
displayName: 'Build Application'
steps:
- checkout: self
- task: Bash@3
displayName: 'Install DSM CLI'
inputs:
targetType: 'inline'
script: |
curl -LO https://github.com/senhasegura/dsmcli/releases/latest/download/dsm-linux-amd64
chmod +x dsm-linux-amd64
sudo mv dsm-linux-amd64 /usr/local/bin/dsm
dsm --version
- task: Bash@3
displayName: 'Fetch Secrets from Senhasegura'
inputs:
targetType: 'inline'
script: |
dsm runb \
--tool-name azure-devops \
--application "$(APPLICATION_NAME)" \
--system "$(SYSTEM_NAME)" \
--environment "$(ENVIRONMENT)"
env:
SENHASEGURA_URL: $(SENHASEGURA_URL)
SENHASEGURA_CLIENT_ID: $(SENHASEGURA_CLIENT_ID)
SENHASEGURA_CLIENT_SECRET: $(SENHASEGURA_CLIENT_SECRET)
- task: Bash@3
displayName: 'Load Secrets'
inputs:
targetType: 'inline'
script: |
# Source secrets and export as pipeline variables
while IFS='=' read -r key value; do
# Skip empty lines and comments
[[ -z "$key" || "$key" =~ ^# ]] && continue
# Set as pipeline variable
echo "##vso[task.setvariable variable=$key;issecret=true]$value"
done < .runb.vars
- task: NodeTool@0
displayName: 'Use Node.js'
inputs:
versionSpec: '20.x'
- task: Bash@3
displayName: 'Install Dependencies'
inputs:
targetType: 'inline'
script: npm ci
- task: Bash@3
displayName: 'Build Application'
inputs:
targetType: 'inline'
script: npm run build
- task: Bash@3
displayName: 'Run Tests'
inputs:
targetType: 'inline'
script: npm test
- task: Bash@3
displayName: 'Cleanup Secrets'
condition: always()
inputs:
targetType: 'inline'
script: rm -f .runb.vars
- task: PublishBuildArtifacts@1
displayName: 'Publish Artifacts'
inputs:
pathToPublish: '$(Build.SourcesDirectory)/dist'
artifactName: 'app'
- stage: Deploy
displayName: 'Deploy to Production'
dependsOn: Build
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: DeployJob
displayName: 'Deploy Application'
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- task: Bash@3
displayName: 'Install DSM CLI'
inputs:
targetType: 'inline'
script: |
curl -LO https://github.com/senhasegura/dsmcli/releases/latest/download/dsm-linux-amd64
chmod +x dsm-linux-amd64
sudo mv dsm-linux-amd64 /usr/local/bin/dsm
- task: Bash@3
displayName: 'Fetch Deploy Secrets'
inputs:
targetType: 'inline'
script: |
dsm runb \
--tool-name azure-devops \
--application "$(APPLICATION_NAME)" \
--system "$(SYSTEM_NAME)" \
--environment "prod"
env:
SENHASEGURA_URL: $(SENHASEGURA_URL)
SENHASEGURA_CLIENT_ID: $(SENHASEGURA_CLIENT_ID)
SENHASEGURA_CLIENT_SECRET: $(SENHASEGURA_CLIENT_SECRET)
- task: Bash@3
displayName: 'Deploy to Kubernetes'
inputs:
targetType: 'inline'
script: |
source .runb.vars
kubectl set image deployment/myapp myapp=$DOCKER_IMAGE
kubectl rollout status deployment/myapp
- task: Bash@3
displayName: 'Cleanup'
condition: always()
inputs:
targetType: 'inline'
script: rm -f .runb.vars
# Senhasegura DSM CLI Configuration
# Location: ~/.senhasegura/config.yaml or specify with SENHASEGURA_CONFIG_FILE
# =============================================================================
# REQUIRED SETTINGS
# =============================================================================
# Your Senhasegura instance URL (no trailing slash)
SENHASEGURA_URL: "https://senhasegura.example.com"
# OAuth 2.0 Client ID from A2A application
SENHASEGURA_CLIENT_ID: "your-oauth2-client-id"
# OAuth 2.0 Client Secret from A2A application
SENHASEGURA_CLIENT_SECRET: "your-oauth2-client-secret"
# =============================================================================
# OPTIONAL SETTINGS
# =============================================================================
# Path to mapping file for registering/updating secrets from pipeline
# SENHASEGURA_MAPPING_FILE: "/path/to/mapping.json"
# Output file for secrets (default: .runb.vars)
SENHASEGURA_SECRETS_FILE: ".runb.vars"
# Disable runb mode (0 = enabled, 1 = disabled)
SENHASEGURA_DISABLE_RUNB: 0
# Skip SSL certificate verification (NOT recommended for production)
# SENHASEGURA_INSECURE: false
# Request timeout in seconds
# SENHASEGURA_TIMEOUT: 30
# =============================================================================
# GITLAB INTEGRATION (Optional)
# =============================================================================
# GitLab access token for variable injection
# GITLAB_ACCESS_TOKEN: "glpat-xxxxxxxxxxxx"
# GitLab API URL
# CI_API_V4_URL: "https://gitlab.example.com/api/v4"
# GitLab project ID
# CI_PROJECT_ID: "12345"
# =============================================================================
# KUBERNETES INTEGRATION (Optional)
# =============================================================================
# Kubernetes secret injection path
# SENHASEGURA_K8S_SECRET_PATH: "/var/run/secrets/senhasegura"
# Kubernetes namespace for secrets
# SENHASEGURA_K8S_NAMESPACE: "default"
# Senhasegura ExternalSecret Examples
# Syncs secrets from Senhasegura DSM to Kubernetes
---
# Example 1: Database Credentials with explicit key mapping
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: database-credentials
namespace: default
labels:
app.kubernetes.io/name: myapp
app.kubernetes.io/component: database
spec:
# How often to sync secrets
refreshInterval: 1h
# Reference to SecretStore
secretStoreRef:
name: senhasegura-dsm
kind: SecretStore
# Target Kubernetes secret configuration
target:
name: db-secret
creationPolicy: Owner
deletionPolicy: Retain
template:
type: Opaque
metadata:
labels:
app.kubernetes.io/name: myapp
annotations:
managed-by: external-secrets
# Map specific keys from Senhasegura
data:
- secretKey: DB_HOST
remoteRef:
key: database-prod # Secret identifier in Senhasegura
property: host
- secretKey: DB_PORT
remoteRef:
key: database-prod
property: port
- secretKey: DB_USERNAME
remoteRef:
key: database-prod
property: username
- secretKey: DB_PASSWORD
remoteRef:
key: database-prod
property: password
- secretKey: DB_NAME
remoteRef:
key: database-prod
property: database
---
# Example 2: API Configuration - Extract all fields
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: api-configuration
namespace: default
spec:
refreshInterval: 30m
secretStoreRef:
name: senhasegura-dsm
kind: SecretStore
target:
name: api-config
creationPolicy: Owner
# Extract all fields from the secret
dataFrom:
- extract:
key: api-settings-prod
# All fields from api-settings-prod become secret keys
---
# Example 3: Multiple secrets combined
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: application-secrets
namespace: default
spec:
refreshInterval: 15m
secretStoreRef:
name: senhasegura-dsm
kind: SecretStore
target:
name: app-secrets
creationPolicy: Owner
data:
# From database secret
- secretKey: DATABASE_URL
remoteRef:
key: database-prod
property: connection_string
# From redis secret
- secretKey: REDIS_URL
remoteRef:
key: redis-prod
property: url
# From JWT secret
- secretKey: JWT_SECRET
remoteRef:
key: jwt-keys-prod
property: secret
# From external API
- secretKey: EXTERNAL_API_KEY
remoteRef:
key: third-party-api
property: api_key
---
# Example 4: Using ClusterSecretStore
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: shared-credentials
namespace: production
spec:
refreshInterval: 1h
secretStoreRef:
name: senhasegura-dsm-global
kind: ClusterSecretStore # Reference ClusterSecretStore
target:
name: shared-secret
creationPolicy: Owner
dataFrom:
- extract:
key: shared-services-prod
---
# Example 5: Secret with templating
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: connection-string
namespace: default
spec:
refreshInterval: 1h
secretStoreRef:
name: senhasegura-dsm
kind: SecretStore
target:
name: connection-strings
creationPolicy: Owner
template:
type: Opaque
data:
# Template the connection string
DATABASE_URL: |
postgresql://{{ .username }}:{{ .password }}@{{ .host }}:{{ .port }}/{{ .database }}?sslmode=require
REDIS_URL: |
redis://:{{ .redis_password }}@{{ .redis_host }}:6379/0
data:
- secretKey: username
remoteRef:
key: database-prod
property: username
- secretKey: password
remoteRef:
key: database-prod
property: password
- secretKey: host
remoteRef:
key: database-prod
property: host
- secretKey: port
remoteRef:
key: database-prod
property: port
- secretKey: database
remoteRef:
key: database-prod
property: database
- secretKey: redis_host
remoteRef:
key: redis-prod
property: host
- secretKey: redis_password
remoteRef:
key: redis-prod
property: password
---
# Example 6: TLS Certificate from Senhasegura
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: tls-certificate
namespace: default
spec:
refreshInterval: 24h # Check daily for certificate rotation
secretStoreRef:
name: senhasegura-dsm
kind: SecretStore
target:
name: app-tls
creationPolicy: Owner
template:
type: kubernetes.io/tls
data:
tls.crt: "{{ .certificate }}"
tls.key: "{{ .private_key }}"
data:
- secretKey: certificate
remoteRef:
key: app-tls-cert-prod
property: certificate
- secretKey: private_key
remoteRef:
key: app-tls-cert-prod
property: private_key
# GitHub Actions Workflow with Senhasegura DSM Integration
# Fetches secrets from Senhasegura and injects them into the pipeline
name: Deploy with Senhasegura Secrets
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
env:
APPLICATION_NAME: my-application
SYSTEM_NAME: production
ENVIRONMENT: prod
jobs:
build-and-deploy:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # For OIDC if needed
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install DSM CLI
run: |
curl -LO https://github.com/senhasegura/dsmcli/releases/latest/download/dsm-linux-amd64
chmod +x dsm-linux-amd64
sudo mv dsm-linux-amd64 /usr/local/bin/dsm
dsm --version
- name: Fetch secrets from Senhasegura
env:
SENHASEGURA_URL: ${{ secrets.SENHASEGURA_URL }}
SENHASEGURA_CLIENT_ID: ${{ secrets.SENHASEGURA_CLIENT_ID }}
SENHASEGURA_CLIENT_SECRET: ${{ secrets.SENHASEGURA_CLIENT_SECRET }}
run: |
dsm runb \
--tool-name github \
--application "$APPLICATION_NAME" \
--system "$SYSTEM_NAME" \
--environment "$ENVIRONMENT"
- name: Load secrets into environment
run: |
# Source the secrets file
source .runb.vars
# Export for subsequent steps
cat .runb.vars >> $GITHUB_ENV
- name: Build application
run: |
# Secrets are now available as environment variables
echo "Building with database at: $DB_HOST"
npm ci
npm run build
- name: Run tests
run: |
npm test
- name: Deploy to production
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: |
# Use secrets for deployment
./scripts/deploy.sh
- name: Cleanup secrets
if: always()
run: |
rm -f .runb.vars
# Clear sensitive env vars
unset DB_PASSWORD
unset API_SECRET
# Alternative: Using direct API calls
api-integration:
runs-on: ubuntu-latest
needs: build-and-deploy
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Get OAuth Token
id: auth
env:
SENHASEGURA_URL: ${{ secrets.SENHASEGURA_URL }}
SENHASEGURA_CLIENT_ID: ${{ secrets.SENHASEGURA_CLIENT_ID }}
SENHASEGURA_CLIENT_SECRET: ${{ secrets.SENHASEGURA_CLIENT_SECRET }}
run: |
TOKEN=$(curl -s -X POST "$SENHASEGURA_URL/iso/oauth2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=$SENHASEGURA_CLIENT_ID" \
-d "client_secret=$SENHASEGURA_CLIENT_SECRET" | jq -r '.access_token')
echo "::add-mask::$TOKEN"
echo "token=$TOKEN" >> $GITHUB_OUTPUT
- name: Get database password
id: db-creds
env:
SENHASEGURA_URL: ${{ secrets.SENHASEGURA_URL }}
TOKEN: ${{ steps.auth.outputs.token }}
run: |
RESPONSE=$(curl -s -X GET "$SENHASEGURA_URL/iso/coe/senha?credentialId=123" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json")
PASSWORD=$(echo $RESPONSE | jq -r '.response.credential.password')
echo "::add-mask::$PASSWORD"
echo "password=$PASSWORD" >> $GITHUB_OUTPUT
- name: Use credentials
env:
DB_PASSWORD: ${{ steps.db-creds.outputs.password }}
run: |
echo "Connecting to database..."
# Use $DB_PASSWORD in your scripts
- name: Release credential custody
if: always()
env:
SENHASEGURA_URL: ${{ secrets.SENHASEGURA_URL }}
TOKEN: ${{ steps.auth.outputs.token }}
run: |
curl -X DELETE "$SENHASEGURA_URL/iso/pam/credential/custody/123" \
-H "Authorization: Bearer $TOKEN"
# GitLab CI Pipeline with Senhasegura DSM Integration
# Fetches secrets from Senhasegura and injects them into the pipeline
stages:
- build
- test
- deploy
variables:
APPLICATION_NAME: my-application
SYSTEM_NAME: production
# SENHASEGURA_URL, SENHASEGURA_CLIENT_ID, SENHASEGURA_CLIENT_SECRET
# should be defined in GitLab CI/CD Variables (Settings > CI/CD > Variables)
# Cache DSM CLI between jobs
.dsm_setup: &dsm_setup
before_script:
- |
if [ ! -f /usr/local/bin/dsm ]; then
curl -LO https://github.com/senhasegura/dsmcli/releases/latest/download/dsm-linux-amd64
chmod +x dsm-linux-amd64
mv dsm-linux-amd64 /usr/local/bin/dsm
fi
dsm --version
build:
stage: build
image: node:20-alpine
<<: *dsm_setup
script:
# Fetch secrets
- |
dsm runb \
--tool-name gitlab \
--application "$APPLICATION_NAME" \
--system "$SYSTEM_NAME" \
--environment "$CI_ENVIRONMENT_NAME"
# Load secrets
- source .runb.vars
# Build
- npm ci
- npm run build
after_script:
- rm -f .runb.vars
artifacts:
paths:
- dist/
expire_in: 1 hour
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
test:
stage: test
image: node:20-alpine
<<: *dsm_setup
script:
# Fetch test secrets
- |
dsm runb \
--tool-name gitlab \
--application "$APPLICATION_NAME" \
--system "$SYSTEM_NAME" \
--environment "test"
- source .runb.vars
- npm ci
- npm test
after_script:
- rm -f .runb.vars
coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
# Deploy to staging
deploy_staging:
stage: deploy
image: alpine:latest
environment:
name: staging
url: https://staging.example.com
<<: *dsm_setup
script:
- apk add --no-cache curl kubectl
# Fetch staging secrets
- |
dsm runb \
--tool-name gitlab \
--application "$APPLICATION_NAME" \
--system "$SYSTEM_NAME" \
--environment "staging"
- source .runb.vars
# Deploy
- kubectl config use-context staging
- kubectl apply -f k8s/
- kubectl rollout status deployment/myapp
after_script:
- rm -f .runb.vars
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
# Deploy to production
deploy_production:
stage: deploy
image: alpine:latest
environment:
name: production
url: https://app.example.com
<<: *dsm_setup
script:
- apk add --no-cache curl kubectl
# Fetch production secrets
- |
dsm runb \
--tool-name gitlab \
--application "$APPLICATION_NAME" \
--system "$SYSTEM_NAME" \
--environment "prod"
- source .runb.vars
# Deploy with approval
- kubectl config use-context production
- kubectl apply -f k8s/
- kubectl rollout status deployment/myapp
after_script:
- rm -f .runb.vars
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual
needs:
- deploy_staging
# Secret rotation job - runs weekly
rotate_secrets:
stage: deploy
image: alpine:latest
<<: *dsm_setup
script:
- apk add --no-cache curl jq
# Get OAuth token
- |
TOKEN=$(curl -s -X POST "$SENHASEGURA_URL/iso/oauth2/token" \
-d "grant_type=client_credentials" \
-d "client_id=$SENHASEGURA_CLIENT_ID" \
-d "client_secret=$SENHASEGURA_CLIENT_SECRET" | jq -r '.access_token')
# Trigger rotation for specific credentials
- |
curl -X POST "$SENHASEGURA_URL/api/pam/credential/123/rotate" \
-H "Authorization: Bearer $TOKEN"
- echo "Credentials rotated successfully"
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
only:
refs:
- schedules
{
"$schema": "https://raw.githubusercontent.com/senhasegura/dsmcli/main/schema/mapping.json",
"_comment": "DSM CLI Mapping File - Maps environment variables to Senhasegura secrets",
"access_keys": [
{
"name": "AWS_PRODUCTION",
"type": "aws",
"description": "AWS credentials for production workloads",
"fields": {
"access_key_id": "AWS_ACCESS_KEY_ID",
"secret_access_key": "AWS_SECRET_ACCESS_KEY"
}
},
{
"name": "AZURE_PRODUCTION",
"type": "azure",
"description": "Azure service principal credentials",
"fields": {
"client_id": "AZURE_CLIENT_ID",
"client_secret": "AZURE_CLIENT_SECRET",
"tenant_id": "AZURE_TENANT_ID",
"subscription_id": "AZURE_SUBSCRIPTION_ID"
}
},
{
"name": "GCP_PRODUCTION",
"type": "gcp",
"description": "Google Cloud service account",
"fields": {
"service_account_json": "GOOGLE_APPLICATION_CREDENTIALS_JSON"
}
}
],
"credentials": [
{
"name": "DATABASE_POSTGRES",
"description": "PostgreSQL database credentials",
"fields": {
"user": "DB_USER",
"password": "DB_PASSWORD",
"host": "DB_HOST",
"port": "DB_PORT",
"database": "DB_NAME"
}
},
{
"name": "DATABASE_REDIS",
"description": "Redis cache credentials",
"fields": {
"host": "REDIS_HOST",
"port": "REDIS_PORT",
"password": "REDIS_PASSWORD"
}
},
{
"name": "DOCKER_REGISTRY",
"description": "Container registry authentication",
"fields": {
"user": "DOCKER_USERNAME",
"password": "DOCKER_PASSWORD",
"host": "DOCKER_REGISTRY_URL"
}
},
{
"name": "SSH_DEPLOY_KEY",
"description": "SSH key for deployment",
"fields": {
"user": "SSH_USER",
"password": "SSH_PRIVATE_KEY",
"host": "SSH_HOST"
}
}
],
"key_value": [
{
"name": "API_TOKENS",
"description": "Various API tokens and secrets",
"fields": [
"API_KEY",
"API_SECRET",
"WEBHOOK_SECRET",
"ENCRYPTION_KEY"
]
},
{
"name": "JWT_CONFIGURATION",
"description": "JWT signing keys",
"fields": [
"JWT_SECRET",
"JWT_PRIVATE_KEY",
"JWT_PUBLIC_KEY"
]
},
{
"name": "THIRD_PARTY_INTEGRATIONS",
"description": "External service API keys",
"fields": [
"STRIPE_SECRET_KEY",
"SENDGRID_API_KEY",
"SLACK_WEBHOOK_URL",
"DATADOG_API_KEY"
]
},
{
"name": "MONITORING",
"description": "Monitoring and observability credentials",
"fields": [
"GRAFANA_API_KEY",
"PROMETHEUS_TOKEN",
"SENTRY_DSN"
]
}
]
}
MCP Integration
The senhasegura MCP server lets Claude Code call PAM Core and DSM operations as tools. The runnable server is Tools/SenhaseguraMcpServer.ts.
Configure Claude Code
Add to ~/.claude/claude_desktop_config.json (Claude Desktop) or .claude/settings.json (Claude Code), pointing at the absolute path of the server:
{
"mcpServers": {
"senhasegura": {
"command": "bun",
"args": [
"run",
"/absolute/path/to/skills/senhasegura/Tools/SenhaseguraMcpServer.ts"
],
"env": {
"SENHASEGURA_URL": "https://senhasegura.example.com",
"SENHASEGURA_CLIENT_ID": "your-client-id",
"SENHASEGURA_CLIENT_SECRET": "your-client-secret"
}
}
}
}PAI uses Bun by convention. If you must run with Node, install tsx and switch the command — but Bun is the standard.
Available tools
Credential management
| Tool | Description | Parameters |
|---|---|---|
senhasegura_list_credentials | List credentials visible to this authorization | — |
senhasegura_get_credential | Get credential metadata | id: string |
senhasegura_get_password | Retrieve password (auto-releases custody) | credentialId: string |
SSH key management
| Tool | Description | Parameters |
|---|---|---|
senhasegura_list_ssh_keys | List all SSH keys | — |
senhasegura_get_ssh_key | Get SSH key (includes private key) | id: string |
senhasegura_rotate_ssh_key | Trigger key rotation | id: string |
DevOps Secret Manager
| Tool | Description | Parameters |
|---|---|---|
senhasegura_dsm_list_secrets | List DSM secrets | application?: string |
senhasegura_dsm_get_secret | Get a DSM secret | identifier: string |
Usage in Claude Code
You: List all credentials in senhasegura
Claude: [calls senhasegura_list_credentials]
Found 15 credentials:
- db-admin-prod (admin@db.example.com)
- api-service-account (svc_api@api.example.com)
...You: Get the password for the production database credential
Claude: [calls senhasegura_list_credentials → finds db-admin-prod]
[calls senhasegura_get_password with credentialId]
The password for db-admin-prod has been retrieved.
Custody released automatically.You: What secrets are available for the payment-service application?
Claude: [calls senhasegura_dsm_list_secrets with application="payment-service"]
Found 4 secrets:
- stripe-api-keys (prod)
- database-credentials (prod)
- jwt-signing-key (prod)
- encryption-keys (prod)Security considerations
- Token caching. Tokens cache in-process and refresh at expiry minus 60s — no 401 retry storms.
- Custody.
senhasegura_get_passwordauto-releases custody infinally. If the release fails, the primary call still returns the secret; operators should monitor senhasegura's audit reports for orphaned custody. - Audit. Every API call is logged in senhasegura's audit logs. Set a meaningful A2A application name so operations are traceable.
- Least privilege. Restrict the A2A authorization to the credentials/secrets the MCP user should reach. Don't share authorizations across humans and machine workloads.
- IP restriction. The IP senhasegura sees is the egress of the host running this MCP server (your laptop, a bastion, the dev container). Whitelist accordingly. (SKILL.md Gotcha #4.)
Debugging
# Quick auth + list test
curl -s -X POST "$SENHASEGURA_URL/iso/oauth2/token" \
-d "grant_type=client_credentials" \
-d "client_id=$SENHASEGURA_CLIENT_ID" \
-d "client_secret=$SENHASEGURA_CLIENT_SECRET"
curl -H "Authorization: Bearer $TOKEN" "$SENHASEGURA_URL/api/pam/credential"If the MCP server fails to start, run it directly to see stderr:
SENHASEGURA_URL=... SENHASEGURA_CLIENT_ID=... SENHASEGURA_CLIENT_SECRET=... \
bun run Tools/SenhaseguraMcpServer.tsThe server speaks stdio MCP — useful errors land on stderr and won't disturb the protocol on stdout.
OAuth 1.0 (Legacy)
Use OAuth 2.0 for any new integration. OAuth 1.0 remains supported for backward compatibility with older A2A applications. This document exists so you can keep an existing OAuth 1.0 path running while migrating.
When you'll see OAuth 1.0
- A2A application created before OAuth 2.0 was the default
- Vendor integrations that haven't been updated
- On-prem systems with strict change-control where rotating to OAuth 2.0 hasn't been approved
Credentials
OAuth 1.0 uses four secrets per client:
consumer_keyconsumer_secrettoken_keytoken_secret
All four come from A2A → Applications → `<app>` → Authorization (the same screen as OAuth 2.0, but the app must be configured with Authentication method: OAuth 1.0).
Python example
from senhasegura import A2A
client = A2A(
base_url="https://senhasegura.example.com",
consumer_key="your-consumer-key",
consumer_secret="your-consumer-secret",
token_key="your-token-key",
token_secret="your-token-secret",
auth_method="oauth1",
)
response = client.get("/iso/coe/senha", params={"credentialId": 123})Migration to OAuth 2.0
1. Create a new A2A application configured for OAuth 2.0 — don't try to flip the existing one. 2. Authorize the new application against the same modules and credentials as the old one (or a tighter subset — migration is a good time to scope down). 3. Update one consumer at a time to point at the new client_id / client_secret. 4. Once all consumers are migrated, disable the old OAuth 1.0 application and remove its authorizations.
Don't run both side-by-side longer than a release cycle — each authorization is a credential surface.
Security notes
- The four-token shape doesn't expire by itself; rotate it on the same cadence as your OAuth 2.0 secrets.
- Audit logs do distinguish OAuth 1.0 vs OAuth 2.0 traffic — useful when verifying migrations.
- HMAC signing in OAuth 1.0 means clock skew matters: keep client clocks within ~5 minutes of the senhasegura server.
"""
Senhasegura A2A API Client for Python
OAuth 2.0 authentication with credential management
"""
import os
import time
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Any, Callable, TypeVar
from contextlib import contextmanager
import requests
from requests.auth import AuthBase
@dataclass
class Credential:
"""Credential model"""
id: str
identifier: str
username: str
hostname: str
ip: str | None = None
credential_type: str = "Local User"
additional_info: str | None = None
tags: list[str] | None = None
@dataclass
class CredentialPassword:
"""Credential password model"""
id: str
password: str
expiration: datetime
class OAuth2Auth(AuthBase):
"""OAuth 2.0 authentication handler for requests"""
def __init__(self, base_url: str, client_id: str, client_secret: str):
self.base_url = base_url
self.client_id = client_id
self.client_secret = client_secret
self.access_token: str | None = None
self.token_expiry: datetime | None = None
def _get_token(self) -> str:
"""Obtain new access token"""
response = requests.post(
f"{self.base_url}/iso/oauth2/token",
data={
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
},
headers={"Content-Type": "application/x-www-form-urlencoded"},
timeout=30,
)
response.raise_for_status()
data = response.json()
self.access_token = data["access_token"]
# Set expiry with 1 minute buffer
self.token_expiry = datetime.now() + timedelta(seconds=data["expires_in"] - 60)
return self.access_token
def __call__(self, request):
if not self.access_token or not self.token_expiry or self.token_expiry < datetime.now():
self._get_token()
request.headers["Authorization"] = f"Bearer {self.access_token}"
return request
class SenhaseguraClient:
"""Senhasegura A2A API Client"""
def __init__(
self,
base_url: str | None = None,
client_id: str | None = None,
client_secret: str | None = None,
timeout: int = 30,
):
self.base_url = base_url or os.environ["SENHASEGURA_URL"]
self.timeout = timeout
self.auth = OAuth2Auth(
self.base_url,
client_id or os.environ["SENHASEGURA_CLIENT_ID"],
client_secret or os.environ["SENHASEGURA_CLIENT_SECRET"],
)
self.session = requests.Session()
self.session.auth = self.auth
def _request(
self,
method: str,
endpoint: str,
params: dict[str, Any] | None = None,
json: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Make authenticated API request"""
response = self.session.request(
method,
f"{self.base_url}{endpoint}",
params=params,
json=json,
timeout=self.timeout,
)
response.raise_for_status()
return response.json()
def list_credentials(self) -> list[Credential]:
"""List all credentials"""
data = self._request("GET", "/api/pam/credential")
return [
Credential(
id=c["id"],
identifier=c["identifier"],
username=c["username"],
hostname=c["hostname"],
ip=c.get("ip"),
credential_type=c.get("type", "Local User"),
additional_info=c.get("additional_info"),
tags=c.get("tags"),
)
for c in data["response"]["credentials"]
]
def get_credential(self, credential_id: str) -> Credential:
"""Get credential by ID"""
data = self._request("GET", f"/api/pam/credential/{credential_id}")
c = data["response"]["credential"]
return Credential(
id=c["id"],
identifier=c["identifier"],
username=c["username"],
hostname=c["hostname"],
ip=c.get("ip"),
credential_type=c.get("type", "Local User"),
)
def get_password(self, credential_id: str) -> CredentialPassword:
"""Get credential password"""
data = self._request(
"GET",
"/iso/coe/senha",
params={"credentialId": credential_id},
)
cred = data["response"]["credential"]
return CredentialPassword(
id=cred["id"],
password=cred["password"],
expiration=datetime.fromisoformat(cred["expiration"].replace("Z", "+00:00")),
)
def create_credential(
self,
identifier: str,
username: str,
password: str,
hostname: str,
**kwargs,
) -> Credential:
"""Create new credential"""
payload = {
"identifier": identifier,
"username": username,
"password": password,
"hostname": hostname,
**kwargs,
}
data = self._request("POST", "/api/pam/credential", json=payload)
c = data["response"]["credential"]
return Credential(
id=c["id"],
identifier=c["identifier"],
username=c["username"],
hostname=c["hostname"],
)
def update_credential(self, credential_id: str, **updates) -> Credential:
"""Update credential"""
data = self._request(
"PUT",
f"/api/pam/credential/{credential_id}",
json=updates,
)
c = data["response"]["credential"]
return Credential(
id=c["id"],
identifier=c["identifier"],
username=c["username"],
hostname=c["hostname"],
)
def release_custody(self, credential_id: str) -> None:
"""Release credential custody"""
self._request("DELETE", f"/iso/pam/credential/custody/{credential_id}")
@contextmanager
def password_context(self, credential_id: str):
"""Context manager for password with automatic custody release"""
cred = self.get_password(credential_id)
try:
yield cred.password
finally:
self.release_custody(credential_id)
# DSM Client for DevOps Secrets
class DSMClient(SenhaseguraClient):
"""Senhasegura DSM (DevOps Secrets Manager) Client"""
def list_secrets(self, application: str | None = None) -> list[dict[str, Any]]:
"""List all secrets"""
params = {}
if application:
params["application"] = application
data = self._request("GET", "/api/dsm/secret", params=params)
return data["response"]["secrets"]
def get_secret(self, identifier: str) -> dict[str, Any]:
"""Get secret by identifier"""
data = self._request("GET", f"/api/dsm/secret/{identifier}")
return data["response"]["secret"]
def create_secret(
self,
identifier: str,
data: dict[str, str],
application: str,
system: str,
environment: str,
) -> dict[str, Any]:
"""Create new secret"""
payload = {
"identifier": identifier,
"data": data,
"application": application,
"system": system,
"environment": environment,
}
response = self._request("POST", "/api/dsm/secret", json=payload)
return response["response"]["secret"]
def update_secret(self, identifier: str, data: dict[str, str]) -> dict[str, Any]:
"""Update secret"""
response = self._request(
"PUT",
f"/api/dsm/secret/{identifier}",
json={"data": data},
)
return response["response"]["secret"]
def delete_secret(self, identifier: str) -> None:
"""Delete secret"""
self._request("DELETE", f"/api/dsm/secret/{identifier}")
def main():
"""Example usage"""
# Initialize client (uses environment variables)
client = SenhaseguraClient()
# List credentials
credentials = client.list_credentials()
for cred in credentials:
print(f" {cred.identifier}: {cred.username}@{cred.hostname}")
# Get password with automatic custody release
with client.password_context("123") as password:
print(f"Using password for connection...")
# Use password here
# DSM example
dsm = DSMClient()
secrets = dsm.list_secrets(application="my-app")
for secret in secrets:
print(f" Secret: {secret['identifier']}")
if __name__ == "__main__":
main()
Python SDK Reference
PAI's standard runtime is Bun + TypeScript — reach for References/TypescriptClient.ts first. This file exists because senhasegura's official examples and many enterprise integrations are still Python.
Available clients
References/PythonClient.py (recommended)
A self-contained client that mirrors TypescriptClient.ts. No PyPI dependency beyond requests.
from PythonClient import SenhaseguraClient, DSMClient
client = SenhaseguraClient() # reads SENHASEGURA_* env vars
# List credentials
for cred in client.list_credentials():
print(f"{cred.identifier}: {cred.username}@{cred.hostname}")
# Get password with automatic custody release (use this, not get_password directly)
with client.password_context("123") as password:
# Use password
pass
# Custody released by __exit__
# DSM
dsm = DSMClient()
for secret in dsm.list_secrets(application="my-app"):
print(secret["identifier"])senhasegura PyPI package (vendor library)
pip install senhasegurafrom senhasegura import A2A
# OAuth 2.0 (recommended)
client = A2A(
base_url="https://senhasegura.example.com",
client_id="your-client-id",
client_secret="your-client-secret",
auth_method="oauth2",
)
response = client.get("/iso/coe/senha", params={"credentialId": 123})
password = response.json()["response"]["credential"]["password"]
# IMPORTANT — release custody after use (Gotcha #1)
client.delete(f"/iso/pam/credential/custody/{credential_id}")Custody safety pattern (mandatory)
Senhasegura's GET /iso/coe/senha auto-locks the credential. Wrap retrieval and release in try/finally:
try:
password = client.get_password(credential_id)
# Use password
finally:
client.release_custody(credential_id)PythonClient.py provides password_context(...) that does this for you. Use it. Raw get_password is for cases where you need fine-grained control.
Error handling
from senhasegura import A2A
from senhasegura.exceptions import AuthenticationError, APIError
try:
client = A2A(
base_url=os.environ["SENHASEGURA_URL"],
client_id=os.environ["SENHASEGURA_CLIENT_ID"],
client_secret=os.environ["SENHASEGURA_CLIENT_SECRET"],
)
response = client.get("/api/pam/credential/999")
response.raise_for_status()
except AuthenticationError as e:
print(f"Authentication failed: {e}")
except APIError as e:
print(f"API error: {e.status_code} - {e.message}")OAuth 1.0 (legacy)
If you must, see References/OAuth1Legacy.md.
When NOT to use Python here
- Inside PAI hooks, skills, or tools — use TypeScript with Bun.
- Inside the MCP server (
Tools/SenhaseguraMcpServer.ts) — TypeScript is the runtime.
This file is here so a Python team or vendor sample can be referenced without polluting the main skill body.
# Senhasegura DSM SecretStore for External Secrets Operator
# Prerequisites:
# 1. External Secrets Operator installed
# 2. Authentication secret created (see auth-secret.yaml)
---
# Authentication Secret
apiVersion: v1
kind: Secret
metadata:
name: senhasegura-auth
namespace: external-secrets
labels:
app.kubernetes.io/name: senhasegura
app.kubernetes.io/component: auth
type: Opaque
stringData:
clientId: "your-oauth2-client-id"
clientSecret: "your-oauth2-client-secret"
---
# Namespace-scoped SecretStore
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: senhasegura-dsm
namespace: default
labels:
app.kubernetes.io/name: senhasegura
app.kubernetes.io/component: secretstore
spec:
provider:
senhasegura:
# Your senhasegura instance URL
url: "https://senhasegura.example.com"
# Module to use: DSM for DevOps Secrets Management
module: DSM
auth:
# Client ID reference
clientId:
secretRef:
name: senhasegura-auth
key: clientId
namespace: external-secrets
# Client Secret reference
clientSecretSecretRef:
name: senhasegura-auth
key: clientSecret
namespace: external-secrets
# Optional: Skip TLS verification (NOT recommended for production)
# ignoreSslCertificate: false
---
# ClusterSecretStore for multi-namespace access
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: senhasegura-dsm-global
labels:
app.kubernetes.io/name: senhasegura
app.kubernetes.io/component: clustersecretstore
spec:
provider:
senhasegura:
url: "https://senhasegura.example.com"
module: DSM
auth:
clientId:
secretRef:
name: senhasegura-auth
key: clientId
namespace: external-secrets
clientSecretSecretRef:
name: senhasegura-auth
key: clientSecret
namespace: external-secrets
# Optional: Restrict which namespaces can use this ClusterSecretStore
conditions:
- namespaceSelector:
matchLabels:
senhasegura-enabled: "true"
/**
* Senhasegura A2A API Client for TypeScript
* OAuth 2.0 authentication with credential management
*/
interface SenhaseguraConfig {
baseUrl: string;
clientId: string;
clientSecret: string;
timeout?: number;
}
interface TokenResponse {
access_token: string;
token_type: string;
expires_in: number;
}
interface Credential {
id: string;
identifier: string;
username: string;
hostname: string;
ip?: string;
type: string;
additional_info?: string;
tags?: string[];
}
interface CredentialPassword {
id: string;
password: string;
expiration: string;
}
interface ApiResponse<T> {
response: {
status: number;
message: string;
error: boolean;
error_code: number;
} & T;
}
export class SenhaseguraClient {
private config: SenhaseguraConfig;
private accessToken: string | null = null;
private tokenExpiry: Date | null = null;
constructor(config: SenhaseguraConfig) {
this.config = {
timeout: 30000,
...config,
};
}
/**
* Authenticate and obtain access token
*/
async authenticate(): Promise<void> {
const response = await fetch(`${this.config.baseUrl}/iso/oauth2/token`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
}),
signal: AbortSignal.timeout(this.config.timeout!),
});
if (!response.ok) {
throw new Error(`Authentication failed: ${response.status} ${response.statusText}`);
}
const data: TokenResponse = await response.json();
this.accessToken = data.access_token;
this.tokenExpiry = new Date(Date.now() + data.expires_in * 1000 - 60000); // 1 min buffer
}
/**
* Ensure we have a valid token
*/
private async ensureAuthenticated(): Promise<void> {
if (!this.accessToken || !this.tokenExpiry || this.tokenExpiry < new Date()) {
await this.authenticate();
}
}
/**
* Make authenticated API request
*/
private async request<T>(
method: string,
endpoint: string,
options?: { body?: unknown; params?: Record<string, string> }
): Promise<T> {
await this.ensureAuthenticated();
const url = new URL(`${this.config.baseUrl}${endpoint}`);
if (options?.params) {
Object.entries(options.params).forEach(([key, value]) => {
url.searchParams.set(key, value);
});
}
const response = await fetch(url.toString(), {
method,
headers: {
Authorization: `Bearer ${this.accessToken}`,
"Content-Type": "application/json",
},
body: options?.body ? JSON.stringify(options.body) : undefined,
signal: AbortSignal.timeout(this.config.timeout!),
});
if (!response.ok) {
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
}
return response.json();
}
/**
* List all credentials
*/
async listCredentials(): Promise<Credential[]> {
const response = await this.request<ApiResponse<{ credentials: Credential[] }>>(
"GET",
"/api/pam/credential"
);
return response.response.credentials;
}
/**
* Get credential by ID
*/
async getCredential(id: string): Promise<Credential> {
const response = await this.request<ApiResponse<{ credential: Credential }>>(
"GET",
`/api/pam/credential/${id}`
);
return response.response.credential;
}
/**
* Get credential password
*/
async getPassword(credentialId: string): Promise<CredentialPassword> {
const response = await this.request<ApiResponse<{ credential: CredentialPassword }>>(
"GET",
"/iso/coe/senha",
{ params: { credentialId } }
);
return response.response.credential;
}
/**
* Create new credential
*/
async createCredential(credential: Omit<Credential, "id">): Promise<Credential> {
const response = await this.request<ApiResponse<{ credential: Credential }>>(
"POST",
"/api/pam/credential",
{ body: credential }
);
return response.response.credential;
}
/**
* Update credential
*/
async updateCredential(id: string, updates: Partial<Credential>): Promise<Credential> {
const response = await this.request<ApiResponse<{ credential: Credential }>>(
"PUT",
`/api/pam/credential/${id}`,
{ body: updates }
);
return response.response.credential;
}
/**
* Release credential custody
*/
async releaseCustody(credentialId: string): Promise<void> {
await this.request("DELETE", `/iso/pam/credential/custody/${credentialId}`);
}
/**
* Get password with automatic custody release
*/
async withPassword<T>(
credentialId: string,
callback: (password: string) => Promise<T>
): Promise<T> {
const { password } = await this.getPassword(credentialId);
try {
return await callback(password);
} finally {
await this.releaseCustody(credentialId);
}
}
}
// Example usage
async function main() {
const client = new SenhaseguraClient({
baseUrl: process.env.SENHASEGURA_URL!,
clientId: process.env.SENHASEGURA_CLIENT_ID!,
clientSecret: process.env.SENHASEGURA_CLIENT_SECRET!,
});
// List all credentials
const credentials = await client.listCredentials();
console.log("Credentials:", credentials);
// Get password with automatic custody release
const result = await client.withPassword("123", async (password) => {
console.log("Using password for database connection...");
// Use password here
return { success: true };
});
console.log("Result:", result);
}
export default SenhaseguraClient;
#!/usr/bin/env bun
/**
* Senhasegura MCP Server
*
* Exposes core senhasegura PAM operations as MCP tools.
* Run via Bun. See ../References/McpIntegration.md for client wiring.
*
* Required environment variables:
* SENHASEGURA_URL
* SENHASEGURA_CLIENT_ID
* SENHASEGURA_CLIENT_SECRET
*/
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
interface SenhaseguraConfig {
baseUrl: string;
clientId: string;
clientSecret: string;
}
class SenhaseguraClient {
private accessToken: string | null = null;
private tokenExpiry: Date | null = null;
constructor(private config: SenhaseguraConfig) {}
async authenticate(): Promise<void> {
const response = await fetch(`${this.config.baseUrl}/iso/oauth2/token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
}),
});
if (!response.ok) {
throw new Error(
`senhasegura auth failed: ${response.status} ${response.statusText}`,
);
}
const data = (await response.json()) as {
access_token: string;
expires_in: number;
};
this.accessToken = data.access_token;
// Refresh at expiry minus 60s — see SKILL.md Gotcha #2
this.tokenExpiry = new Date(Date.now() + data.expires_in * 1000 - 60_000);
}
private async ensureAuth(): Promise<void> {
if (
!this.accessToken ||
!this.tokenExpiry ||
this.tokenExpiry < new Date()
) {
await this.authenticate();
}
}
async request(
method: string,
endpoint: string,
body?: unknown,
): Promise<unknown> {
await this.ensureAuth();
const response = await fetch(`${this.config.baseUrl}${endpoint}`, {
method,
headers: {
Authorization: `Bearer ${this.accessToken}`,
"Content-Type": "application/json",
},
body: body ? JSON.stringify(body) : undefined,
});
if (!response.ok) {
throw new Error(
`senhasegura ${method} ${endpoint} failed: ${response.status} ${response.statusText}`,
);
}
return response.json();
}
async releaseCustody(credentialId: string): Promise<void> {
await this.request("DELETE", `/iso/pam/credential/custody/${credentialId}`);
}
}
const requireEnv = (name: string): string => {
const v = process.env[name];
if (!v) throw new Error(`Missing required env var: ${name}`);
return v;
};
const client = new SenhaseguraClient({
baseUrl: requireEnv("SENHASEGURA_URL"),
clientId: requireEnv("SENHASEGURA_CLIENT_ID"),
clientSecret: requireEnv("SENHASEGURA_CLIENT_SECRET"),
});
const server = new Server(
{ name: "senhasegura-mcp", version: "1.0.0" },
{ capabilities: { tools: {} } },
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "senhasegura_list_credentials",
description: "List all credentials visible to this A2A authorization.",
inputSchema: { type: "object", properties: {} },
},
{
name: "senhasegura_get_credential",
description: "Get credential metadata by ID (no password).",
inputSchema: {
type: "object",
properties: {
id: { type: "string", description: "Credential ID" },
},
required: ["id"],
},
},
{
name: "senhasegura_get_password",
description:
"Retrieve the password for a credential. Custody is released automatically after retrieval.",
inputSchema: {
type: "object",
properties: {
credentialId: { type: "string", description: "Credential ID" },
},
required: ["credentialId"],
},
},
{
name: "senhasegura_list_ssh_keys",
description: "List all SSH keys registered in PAM Core.",
inputSchema: { type: "object", properties: {} },
},
{
name: "senhasegura_get_ssh_key",
description: "Get an SSH key by ID (includes private key).",
inputSchema: {
type: "object",
properties: {
id: { type: "string", description: "SSH key ID" },
},
required: ["id"],
},
},
{
name: "senhasegura_rotate_ssh_key",
description: "Trigger immediate rotation for an SSH key.",
inputSchema: {
type: "object",
properties: {
id: { type: "string", description: "SSH key ID" },
},
required: ["id"],
},
},
{
name: "senhasegura_dsm_list_secrets",
description:
"List secrets in DevOps Secret Manager, optionally filtered by application.",
inputSchema: {
type: "object",
properties: {
application: {
type: "string",
description: "Optional application filter",
},
},
},
},
{
name: "senhasegura_dsm_get_secret",
description: "Get a DSM secret by identifier.",
inputSchema: {
type: "object",
properties: {
identifier: { type: "string", description: "Secret identifier" },
},
required: ["identifier"],
},
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const a = args as Record<string, string | undefined>;
const text = (data: unknown) => ({
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
});
switch (name) {
case "senhasegura_list_credentials": {
return text(await client.request("GET", "/api/pam/credential"));
}
case "senhasegura_get_credential": {
if (!a.id) throw new Error("id is required");
return text(await client.request("GET", `/api/pam/credential/${a.id}`));
}
case "senhasegura_get_password": {
const credentialId = a.credentialId;
if (!credentialId) throw new Error("credentialId is required");
try {
return text(
await client.request(
"GET",
`/iso/coe/senha?credentialId=${encodeURIComponent(credentialId)}`,
),
);
} finally {
// Always release custody — see SKILL.md Gotcha #1
await client.releaseCustody(credentialId).catch(() => {
// Swallow release errors; the primary call result has already been returned.
// Operators should monitor for orphaned custody via senhasegura's audit reports.
});
}
}
case "senhasegura_list_ssh_keys": {
return text(await client.request("GET", "/api/pam/sshkey"));
}
case "senhasegura_get_ssh_key": {
if (!a.id) throw new Error("id is required");
return text(await client.request("GET", `/api/pam/sshkey/${a.id}`));
}
case "senhasegura_rotate_ssh_key": {
if (!a.id) throw new Error("id is required");
return text(
await client.request("POST", `/api/pam/sshkey/${a.id}/rotate`),
);
}
case "senhasegura_dsm_list_secrets": {
const endpoint = a.application
? `/api/dsm/secret?application=${encodeURIComponent(a.application)}`
: "/api/dsm/secret";
return text(await client.request("GET", endpoint));
}
case "senhasegura_dsm_get_secret": {
if (!a.identifier) throw new Error("identifier is required");
return text(
await client.request(
"GET",
`/api/dsm/secret/${encodeURIComponent(a.identifier)}`,
),
);
}
default:
throw new Error(`Unknown tool: ${name}`);
}
});
const transport = new StdioServerTransport();
await server.connect(transport);
Troubleshooting
Failure modes you'll hit, mapped to causes and fixes. The Gotchas in SKILL.md cover the most common ones — this file expands on them and adds debug procedures.
Authentication errors
| Error | Cause | Fix |
|---|---|---|
401 Unauthorized | Token expired or invalid | Request a new token; verify token caching uses expires_in − 60s (Gotcha #2) |
403 Forbidden | Authorization missing or filter excludes the resource | Check A2A → Authorizations in the console |
invalid_client | Wrong client_id / client_secret | Re-copy from console; whitespace in env vars is a frequent cause |
IP not allowed | Source IP doesn't match the authorization restriction (Gotcha #4) | Whitelist the actual egress IP — for K8s, that's the cluster NAT, not the pod |
404 on /iso/oauth2/token | Wrong path scheme (Gotcha #9) | Path is /iso/oauth2/token, not /api/oauth2/token |
DSM CLI issues
Debug mode
dsm runb --debug \
--application myapp \
--system prod \
--environment prodCommon fixes
| Symptom | Fix |
|---|---|
| Config file not found | export SENHASEGURA_CONFIG_FILE=/absolute/path/to/config.yaml |
| SSL certificate errors (corporate CA) | Mount the CA bundle into the runner; only set SENHASEGURA_INSECURE: true for local debugging (Gotcha #10) |
| Permission denied on config | chmod 600 ~/.senhasegura/config.yaml |
.runb.vars accidentally committed | Run from a tmpdir or set SENHASEGURA_SECRETS_FILE=/tmp/runb.$$.vars (Gotcha #5); rotate any exposed secrets |
| Secrets visible in CI log output | Wrong --tool-name for the runner (Gotcha #7) |
External Secrets Operator
# SecretStore status
kubectl describe secretstore senhasegura-dsm
# ExternalSecret status
kubectl describe externalsecret database-credentials
# ESO logs
kubectl logs -n external-secrets -l app.kubernetes.io/name=external-secrets| ESO log message | Likely cause |
|---|---|
could not get provider client | Auth secret missing/wrong, OR cluster egress IP not in A2A IP whitelist (Gotcha #4) |
provider not found | Casing wrong — senhasegura lowercase, DSM uppercase (Gotcha #6) |
could not find secret <X> | remoteRef.key doesn't match a DSM secret identifier |
refresh failed | Network/firewall to senhasegura, OR token endpoint unreachable |
| TLS verify error | Internal CA not trusted by the ESO pod — mount it via ca field |
API response codes
| Code | Meaning | Action |
|---|---|---|
200 | Success | Process the response |
400 | Bad request | Validate JSON shape and required fields |
401 | Unauthorized | Re-authenticate; check token caching |
403 | Forbidden | Check A2A authorization permissions and credential filter |
404 | Not found | Verify resource ID; the authorization may not see it |
429 | Rate limited | Implement exponential backoff with jitter (see below) |
5xx | Server error | Retry with backoff; persistent → contact senhasegura support |
Rate limits
Senhasegura applies rate limits per A2A authorization. Limits are not published as a fixed number — they vary by deployment and license. Practical guidance:
- For batch enumerations (e.g. listing all credentials), paginate and add a 50–200 ms sleep between pages.
- On
429, sleepmin(60, 2^attempt + random(0, 1))seconds and retry up to 5 times. - Token requests should be cached and reused across the whole job — every
dsm runbinvocation in a long pipeline burns one token call. - For ESO,
refreshInterval: 1his typical; do not drop below 5m without an operational reason. - Audit logs surface throttling events — review Reports → API Audit if you suspect throttling.
Connection / network
| Symptom | Check |
|---|---|
Connection refused | DNS resolution, firewall outbound rules, senhasegura listening ports |
Connection reset | Mid-flight, often a stateful firewall idle timeout — shorten request payload or split work |
| Long tail latency | Senhasegura behind WAF/proxy with cold paths; warm with a periodic heartbeat call |
Audit and observability
- Reports → API Audit in the console is the source of truth for what your A2A application called and what it received (200/4xx/5xx).
- Reports → Access Logs show password retrievals and custody events — useful when you suspect Gotcha #1 (forgotten custody release).
- Plumb request/response timing into your own observability — senhasegura latency is invisible to most apps until it isn't.
Workflow: Inject Senhasegura Secrets into CI/CD
Fetch DSM secrets at pipeline runtime via dsm runb, source them as environment variables, and clean up before the job ends.
Prerequisites
- DSM CLI available in the runner image
- A2A OAuth 2.0 credentials (
Workflows/SetupA2A.md), stored as masked CI/CD secrets - Application/system/environment registered in DSM
Steps
1. Store senhasegura credentials in the CI/CD platform
| Platform | Where |
|---|---|
| GitHub Actions | Settings → Secrets and variables → Actions → SENHASEGURA_URL, SENHASEGURA_CLIENT_ID, SENHASEGURA_CLIENT_SECRET |
| GitLab CI | Settings → CI/CD → Variables (mark as masked + protected) |
| Azure DevOps | Pipelines → Library → Variable group senhasegura-credentials (linked from each pipeline) |
| Jenkins | Credentials → Add → secret text for each variable |
2. Install the DSM CLI in the runner
curl -LO https://github.com/senhasegura/dsmcli/releases/latest/download/dsm-linux-amd64
chmod +x dsm-linux-amd64
sudo mv dsm-linux-amd64 /usr/local/bin/dsm
dsm --versionFor GitHub Actions, see References/GithubActionsExample.yaml. For Azure DevOps, see References/AzurePipelinesExample.yaml. For GitLab, see References/GitlabCiExample.yaml.
3. Fetch secrets
Run from a temp directory (Gotcha #5 — never let .runb.vars land in the repo root):
mkdir -p /tmp/senhasegura && cd /tmp/senhasegura
dsm runb \
--tool-name github \ # or azure-devops / gitlab / linux
--application my-app \
--system production \
--environment prod--tool-name controls log masking — match the actual runner. Mismatched values can leak secrets to the log (Gotcha #7).
4. Source secrets into the environment
source /tmp/senhasegura/.runb.vars
# In GitHub Actions, you can also export across steps:
cat /tmp/senhasegura/.runb.vars >> "$GITHUB_ENV"5. Use and clean up
echo "Deploying with database host: $DB_HOST"
./deploy.shCleanup must always run, even on failure:
# GitHub Actions
- name: Cleanup
if: always()
run: rm -f /tmp/senhasegura/.runb.vars# Azure DevOps
- script: rm -f /tmp/senhasegura/.runb.vars
displayName: Cleanup
condition: always()# GitLab CI
after_script:
- rm -f /tmp/senhasegura/.runb.varsMapping file (optional — write secrets back from CI)
For pipelines that register or update secrets, use a mapping file. See References/MappingExample.json for the full shape:
{
"credentials": [
{
"name": "DATABASE_CREDS",
"fields": {
"user": "DB_USER",
"password": "DB_PASSWORD",
"host": "DB_HOST"
}
}
]
}Set SENHASEGURA_MAPPING_FILE=/path/to/mapping.json and DSM will sync changes.
Common failures
| Symptom | Cause |
|---|---|
command not found: dsm | CLI not installed in the runner |
Empty .runb.vars | Application/system/environment combination doesn't exist in DSM |
| Secrets visible in log output | Wrong --tool-name (Gotcha #7) |
.runb.vars committed to repo | Ran from repo root (Gotcha #5) — switch to a tmpdir |
401 mid-pipeline | Token expired during a long-running step — fetch right before use, not at job start |
Workflow: Register and Rotate SSH Keys
Register an SSH key pair in senhasegura, configure automatic rotation, and consume the key from a CI/CD or operations workflow.
Prerequisites
- Senhasegura console access with PAM Core / SSH Keys permissions
- Target devices (the systems whose
authorized_keyswill be updated) reachable from senhasegura - A2A OAuth 2.0 credentials if you'll fetch via API (
Workflows/SetupA2A.md)
Steps
1. Generate the SSH key pair (if you don't already have one)
ssh-keygen -t ed25519 -C "deploy@example.com" -f ~/.ssh/deploy_key
# Output:
# ~/.ssh/deploy_key (private)
# ~/.ssh/deploy_key.pub (public)Prefer Ed25519 over RSA. Use a strong passphrase or none — the key will be stored encrypted in senhasegura either way.
2. Register in senhasegura
Console: PAM Core → Credentials → SSH Keys → New
- Identifier:
deploy-key-prod - Username:
deploy - Device:
*.prod.example.com(host pattern) - Private key: paste contents of
deploy_key - Public key: paste contents of
deploy_key.pub - Passphrase: if applicable
Rotation settings:
- Enable automatic renewal: Yes
- Renewal period: 90 days
- Key type: Ed25519
3. Configure target devices
Console: PAM Core → Credentials → SSH Keys → deploy-key-prod → Devices tab
Add each host that should receive the new public key on rotation. Ensure for each:
- Reachable from senhasegura on TCP/22 (or your chosen SSH port)
- SSH service running
authorized_keyspath correct (default~deploy/.ssh/authorized_keys)- The current key is already trusted so senhasegura can SSH in to install the next one
4. Retrieve the key via API
TOKEN=$(curl -s -X POST "$SENHASEGURA_URL/iso/oauth2/token" \
-d "grant_type=client_credentials" \
-d "client_id=$SENHASEGURA_CLIENT_ID" \
-d "client_secret=$SENHASEGURA_CLIENT_SECRET" | jq -r '.access_token')
# List SSH keys
curl -H "Authorization: Bearer $TOKEN" "$SENHASEGURA_URL/api/pam/sshkey"
# Get a specific key
curl -H "Authorization: Bearer $TOKEN" "$SENHASEGURA_URL/api/pam/sshkey/456"
# Use it
curl -s -H "Authorization: Bearer $TOKEN" \
"$SENHASEGURA_URL/api/pam/sshkey/456" \
| jq -r '.private_key' > /tmp/deploy_key
chmod 600 /tmp/deploy_key
ssh -i /tmp/deploy_key deploy@target.prod.example.com
rm -f /tmp/deploy_key # clean up5. Trigger rotation manually (if needed)
Console: SSH Keys → deploy-key-prod → Actions → Rotate.
API:
curl -X POST "$SENHASEGURA_URL/api/pam/sshkey/456/rotate" \
-H "Authorization: Bearer $TOKEN"On rotation senhasegura:
1. Generates a new key pair. 2. Connects to each target device with the current key and appends the new public key. 3. Stores the new private key. 4. Records the operation in audit logs. 5. After verification, removes the old key from the targets.
If step 2 fails on any device, the rotation is aborted — the existing key stays valid.
Common failures
| Symptom | Cause |
|---|---|
| Rotation aborts on one host | Senhasegura can't SSH there with the current key — check network, sshd config, key trust |
| New key doesn't work post-rotation | Wrong authorized_keys path, or sshd has AuthorizedKeysCommand overriding the file |
Permission denied (publickey) after retrieving via API | Forgot chmod 600, OR file owner mismatch in containers |
Next workflows
- Embed retrieval in a deploy pipeline →
Workflows/InjectCiCdSecrets.md - Use the MCP server to expose key retrieval to Claude Code →
References/McpIntegration.md
Workflow: Automated Password Rotation
Configure scheduled, automated password rotation for credentials managed by senhasegura.
Prerequisites
- Senhasegura console access with Executions module permissions
- Target hosts reachable from senhasegura with required protocol (SSH/WinRM/etc.)
- Notification email (or webhook) for rotation outcomes
Steps
1. Configure an Execution Template
Console: Executions → Templates → New
- Name:
Linux Password Change - Executor: SSH
- Plugin: Linux
- Credential type: Local User
- Commands (example):
echo '[#NEW_PASSWORD#]' | passwd --stdin [#USERNAME#]- Verification: SSH login test with the new password
[#NEW_PASSWORD#] and [#USERNAME#] are senhasegura placeholders, substituted at execution time.
2. Create an Execution Policy
Console: Executions → Policies → New
- Name:
Monthly Linux Password Rotation - Status: Active
- Credentials: filter by
tag:linux-servers(or any tag/group expression) - Template:
Linux Password Change - Schedule: Frequency
Monthly, Day1, Time02:00 AM(use a quiet window) - Notifications: email
security@example.comon Success, Failure
3. Test rotation manually
Via console: PAM Core → Credentials → <credential> → Actions → Rotate.
Via API:
curl -X POST "$SENHASEGURA_URL/api/pam/credential/123/rotate" \
-H "Authorization: Bearer $TOKEN"Verify:
- Executions → History shows a successful run for the credential
- A login test against the host with the new password succeeds
4. Monitor execution history
Console: Executions → History — filter by date or status. Watch for:
- Connection timeouts → check network/firewall reachability
- Authentication failures → senhasegura's stored password drifted from reality
- Command failures → review template syntax against the host's shell
5. Roll out across the fleet
After one credential proves stable, expand the tag/filter to cover the rest. Stage in waves; do not flip every host the same night.
Tips
- Rotate during a low-traffic window — application restarts may follow.
- Pair rotation with consumer hot-reload (External Secrets Operator + a controller that watches
Secretfor change), so apps don't go stale. - Audit: keep
Executions → Historyretention long enough to satisfy compliance.
Next workflows
- Pull rotated credentials in CI/CD →
Workflows/InjectCiCdSecrets.md - Mirror rotated credentials into Kubernetes →
Workflows/SyncKubernetesSecrets.md
Workflow: Setup A2A Application
Configure a new A2A application for API integration with OAuth 2.0 authentication.
Prerequisites
- Senhasegura console access with admin privileges
- Target application/system identified
- Source IP range you'll call from (for IP restriction)
Steps
1. Create A2A application
In the senhasegura console: A2A → Applications → New
- Name:
my-app-integration - Authentication method: OAuth 2.0
- Status: Enabled
- Description: Integration for production workloads
- Save.
2. Retrieve OAuth credentials
A2A → Applications → my-app-integration → Authorization
Copy and store:
client_id— UUID formatclient_secret— long secret string
Store in a secure location (1Password, Vault, encrypted env file). Never commit.
3. Configure authorization rules
A2A → Authorizations → New
- Application:
my-app-integration - Module:
PAM Core(orDSMfor DevOps secrets) - Permission: Read and Write
- IP Restriction:
10.0.0.0/8(the actual source IP senhasegura sees — see SKILL.md Gotcha #4) - Credential filter:
tag:production(optional but recommended — least privilege)
Best practice: one A2A authorization per application. Don't share authorizations across services.
4. Test authentication
export SENHASEGURA_URL="https://senhasegura.example.com"
export SENHASEGURA_CLIENT_ID="..."
export SENHASEGURA_CLIENT_SECRET="..."
curl -s -X POST "$SENHASEGURA_URL/iso/oauth2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=$SENHASEGURA_CLIENT_ID" \
-d "client_secret=$SENHASEGURA_CLIENT_SECRET"Expected response:
{
"access_token": "eyJhbGci...",
"token_type": "Bearer",
"expires_in": 3600
}5. Verify API access
TOKEN=$(curl -s -X POST "$SENHASEGURA_URL/iso/oauth2/token" \
-d "grant_type=client_credentials" \
-d "client_id=$SENHASEGURA_CLIENT_ID" \
-d "client_secret=$SENHASEGURA_CLIENT_SECRET" | jq -r '.access_token')
curl -H "Authorization: Bearer $TOKEN" "$SENHASEGURA_URL/api/pam/credential"Expected: JSON list of credentials this authorization can see.
Common failures
| Symptom | Likely cause |
|---|---|
invalid_client | Wrong client_id or client_secret — re-copy from console |
IP not allowed | Source IP doesn't match restriction (Gotcha #4 — check egress NAT for Kubernetes) |
| Empty credentials list | Authorization filter too restrictive, or no credentials match the tag |
404 on /iso/oauth2/token | Note /iso/, not /api/ (Gotcha #9) |
Next workflows
- Add the credentials to a CI/CD pipeline →
Workflows/InjectCiCdSecrets.md - Sync them into Kubernetes via ESO →
Workflows/SyncKubernetesSecrets.md - Wire up to Claude Code via the MCP server →
References/McpIntegration.md
Workflow: Sync Senhasegura DSM Secrets to Kubernetes
Pull secrets from senhasegura DSM into Kubernetes Secret objects via External Secrets Operator (ESO).
Prerequisites
- Kubernetes cluster access with permissions to install controllers
- Helm 3.x
- A2A OAuth 2.0 credentials (
Workflows/SetupA2A.md) - Cluster egress IP whitelisted in the A2A authorization (SKILL.md Gotcha #4)
Steps
1. Install External Secrets Operator
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets \
-n external-secrets --create-namespace --wait
# Verify
kubectl get pods -n external-secrets2. Create the auth Secret
kubectl create secret generic senhasegura-auth \
-n external-secrets \
--from-literal=clientId="$SENHASEGURA_CLIENT_ID" \
--from-literal=clientSecret="$SENHASEGURA_CLIENT_SECRET"3. Create a SecretStore
Apply References/SecretStoreExample.yaml (edit url and namespaces as needed):
kubectl apply -f References/SecretStoreExample.yamlThe minimum shape:
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: senhasegura-dsm
namespace: default
spec:
provider:
senhasegura: # lowercase — Gotcha #6
url: "https://senhasegura.example.com"
module: DSM # uppercase — Gotcha #6
auth:
clientId:
secretRef:
name: senhasegura-auth
key: clientId
namespace: external-secrets
clientSecretSecretRef:
name: senhasegura-auth
key: clientSecret
namespace: external-secrets
# In production, mount the CA bundle. Don't ignoreSslCertificate. (Gotcha #10)For multi-namespace use a ClusterSecretStore instead — see Dsm.md.
4. Create an ExternalSecret
Apply References/ExternalSecretExample.yaml. Two patterns:
Explicit keys:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: database-credentials
spec:
refreshInterval: 1h
secretStoreRef:
name: senhasegura-dsm
kind: SecretStore
target:
name: db-secret
data:
- secretKey: password
remoteRef:
key: database-prod
property: passwordExtract all fields:
spec:
dataFrom:
- extract:
key: api-settings-prod5. Verify synchronization
kubectl get externalsecret database-credentials
# Expected STATUS: SecretSynced
kubectl get secret db-secret -o yaml
# Expected: data.password is base64-encoded value from senhasegura
# If it's not syncing:
kubectl describe externalsecret database-credentials
kubectl logs -n external-secrets -l app.kubernetes.io/name=external-secretsCommon failures
| Symptom | Likely cause |
|---|---|
could not get provider client | Auth secret wrong, OR cluster egress IP not whitelisted (Gotcha #4) |
provider not found | Casing wrong — senhasegura lowercase, DSM uppercase (Gotcha #6) |
could not find secret | Identifier in remoteRef.key doesn't match a DSM secret |
| TLS errors | Internal CA — mount the CA bundle into ESO pod via ca field, don't use ignoreSslCertificate (Gotcha #10) |
Next workflows
- Set up automated rotation of source credentials →
Workflows/RotatePasswords.md - Wire pipelines that consume these secrets directly →
Workflows/InjectCiCdSecrets.md