Manage Secrets
The current secret provider uses a SOPS-encrypted JSON document and host credentials, keeping the decryption key outside the workspace VM.
[secrets.providers.project]
kind = "sops"
file = "secrets.json"
The path is relative to the workspace configuration directory and defaults to
secrets.json. The decrypted document must be an object with string values.
Use Workspace → Settings → Secrets to reveal, set, or delete values.
Initialize Developer-Service Tokens
The workspace creation page can install GitHub CLI (gh) or GitLab CLI
(glab) and initialize its token. Use a GitHub token with read-only
permissions or a GitLab token with the read_api scope. Tascarrel cannot
validate the token’s authority without contacting the provider, so the selected
permissions remain your responsibility.
Hostd selects $HOME/.ssh/id_ed25519.pub, falling back to
$HOME/.ssh/id_rsa.pub, and writes the corresponding SSH recipient into
.sops.yaml. It encrypts the values into secrets.json and verifies a
decrypting round trip before atomically publishing the workspace. The matching
private-key file must therefore be usable non-interactively by hostd. SOPS’s
SSH-key support cannot decrypt through ssh-agent alone.
The generated GH_TOKEN or GITLAB_TOKEN environment value is only a
placeholder. Explicit HTTP rules replace that placeholder after a matching
request leaves the VM. GitHub’s rule admits POST because gh uses GraphQL
for read operations; the token’s provider permissions enforce the read-only
boundary.
Expose a Process Variable
[env]
API_TOKEN = "${secrets.project.API_TOKEN}"
Environment interpolation deliberately exposes plaintext to workspace processes. The value is resolved at workspace startup, so provider or reference configuration changes require a restart.
Use an SSH Identity Without Exposing Its Private Key
Store a passphrase-free OpenSSH private-key document as a multiline secret, then bind it to one or more destinations. The secret provider still encrypts the document at rest:
[secrets.providers.project]
kind = "sops"
file = "secrets.json"
[ssh-agent]
known-hosts = [
"git.example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...",
]
[ssh-agent.identities.source-control]
secret = "project.SSH_PRIVATE_KEY"
destinations = ["git@git.example.com"]
[network]
allow-ports = [22, 80, 443]
Use Workspace → Settings → Secrets to store the private key under
project.SSH_PRIVATE_KEY. Tascarrel decrypts it only on the host and loads it
into an ephemeral OpenSSH agent for each pod connection. Pod processes receive
SSH_AUTH_SOCK=/run/tascarrel/ssh-agent.sock; the private-key document never
enters the VM. The agent exposes the public identity and permits signatures only
after a compatible OpenSSH client binds the connection to a configured server.
Agent mutation operations are rejected at the VM boundary.
The known-hosts field pins the server keys used to enforce the destination
constraints. Obtain each entry through a trusted channel; an unchecked
ssh-keyscan result does not authenticate a server. This constraint list does
not replace SSH host verification inside the pod, so install the same trusted
server key in the pod image or another pod-side known_hosts file. Network
policy must also admit the destination and SSH port.
Enabling the agent adds SSH_AUTH_SOCK to newly started pod processes. Restart
the workspace after enabling it. Once enabled, identity, destination, host-key,
and secret-value changes apply to the next agent connection.
Use Secretless HTTP Authentication
Keep the credential value on the host by sending a placeholder:
[[network.secret-injection]]
host = "api.example.com"
paths = ["/v1/models", "/v1/projects/*/models"]
methods = ["GET", "HEAD"]
header = "authorization"
placeholder = "replace-with-api-token"
secret = "project.API_TOKEN"
Tascarrel replaces the placeholder in matching GET and HEAD HTTP/1.1
requests after they leave the workspace, so the credential never enters the
workspace VM. It rejects other paths and methods for api.example.com at the
host proxy. HTTPS uses a per-workspace CA installed into common certificate
bundles.
Set header whenever possible; otherwise, Tascarrel checks every eligible
non-routing header. Every injection rule must list at least one syntactically
valid, case-sensitive HTTP method. Host rules accept an exact name or
*.example.com pattern. Optional paths is a non-empty array of absolute path
glob patterns. The usual *, **, ?, character-class, and brace-alternative
forms are available. A single * does not cross /, while ** does. Request
query strings do not participate in matching. Omit paths to match every path;
a literal "/mcp" remains an exact match. When several rules match a host, a
request is admitted if at least one rule matches its path and method, and only
rules admitting the complete request can inject. Rule and
provider-configuration changes apply to new TCP flows after the network policy
reloads; active flows retain their original rules. Updating a value in an
existing provider takes effect on the next matching request without restarting
the VM.
Authenticate Tasci Traffic
Tasci sends a non-secret header template. The host network proxy replaces its placeholder, so the credential never enters the workspace VM or pod. HTTPS endpoints are verified through the workspace system trust store, which includes the Tascarrel workspace certificate authority.
Configure the endpoint under Workspace → Settings → Tasci:
{
"authorization": {
"header": "Authorization",
"value": "Bearer tascarrel-secret:model-api-token"
}
}
Then configure the same placeholder for the provider host in config.toml:
[[network.secret-injection]]
host = "api.example.com"
paths = ["/v1/chat/completions"]
methods = ["POST"]
header = "authorization"
placeholder = "tascarrel-secret:model-api-token"
secret = "project.MODEL_API_TOKEN"
The OpenAI-compatible Chat Completions protocol uses POST, so the rule must
admit that method. A placeholder mismatch leaves the template unchanged and
normally causes the provider to reject the request.
Authenticate MCP Traffic
Model Context Protocol (MCP) servers accept an arbitrary map of header
templates under chat.mcpServers:
{
"private-tools": {
"endpoint": "https://mcp.example.com/mcp",
"headers": {
"Authorization": "Bearer tascarrel-secret:mcp-api-token",
"X-Workspace": "development"
}
}
}
Configure one secret-injection rule for each placeholder-bearing header:
[[network.secret-injection]]
host = "mcp.example.com"
paths = ["/mcp"]
methods = ["GET", "POST", "DELETE"]
header = "authorization"
placeholder = "tascarrel-secret:mcp-api-token"
secret = "project.MCP_API_TOKEN"
Streamable HTTP MCP connections use POST and may also use GET and DELETE,
so authenticated servers commonly admit all three methods.