Graph-Native Secrets Architecture
Handling secrets securely in a graph environment means cryptographic material should ideally not exist as plaintext properties in your source CSVs or the resulting JSON graph.
Transitioning to a Graph-Native Secrets Architecture using Secret Resources and Vault Resources turns cryptographic material into first-class graph citizens without exposing sensitive data. The graph acts as the Control Plane, while an external Vault acts as the Data Plane.
The Core Ontology
In reality the following practice has been proven. Instead of placing a password property on a server or database resource, introducing two dedicated Resource Types and two dedicated Relation Types:
Resources
secret(Logical Resource): Represents the concept of a secret. It holds metadata (like description, rotation policy, or owner) but never the cryptographic material itself.vault(Backend Resource): Represents the secure storage boundary. This is an external system like HashiCorp Vault or AWS Secrets Manager.
Relations
USES_SECRET: Maps consumers (e.g., Database, Application) to the logical secret. This relation can define how the secret is injected (e.g.,injected_as = "DB_PASSWORD").STORED_IN: Maps the logical secret to its physical storage in the vault. This relation contains the persistence layer routing data (e.g., the Vault KV path).
Architecture Diagram
graph LR
App[Application] -- USES_SECRET<br>injected_as: API_TOKEN --> Sec1[Secret: api-token]
DB[Database] -- USES_SECRET<br>injected_as: DB_PASS --> Sec2[Secret: db-password]
Sec1 -- STORED_IN<br>path: kv/data/apps/token --> Vlt[Vault: enterprise-vault]
Sec2 -- STORED_IN<br>path: kv/data/dbs/pass --> Vlt
Implementation Example
Using TOML models, you can dynamically generate the entire secrets topology based on your infrastructure.
1. Creating the Logical Secret
When a resource requires a password or key, define a model that automatically generates the secret resource and links it back to the resource.
# models/instance_secrets.toml
origin_resource = "instance"
[[create_resource]]
resource_type = "secret"
name = "secret-db-{{ origin_resource.name }}"
relation_type = "USES_SECRET"
[create_resource.relation_properties]
injected_as = "DB_PASS"
role = "Reader"
[create_resource.properties]
description = "Database password for {{ origin_resource.name }}"
2. Linking to the Vault
Map the generated secrets to the vault resource. This model acts as the router, telling the graph exactly where the physical secret resides in the backend.
# models/secret_links.toml
origin_resource = "secret"
[[link_resources]]
with = "vault"
match_with = [{ property = "name", value = "enterprise-vault" }]
create_relation = { type = "STORED_IN", properties = { path = "kv/data/secrets/{{ origin_resource.name }}" } }
3. Retrieving Secrets via Output Artifacts
Because Rescile holds no cryptographic material, your output templates (e.g., provisioning scripts, Dockerfiles, or Terraform) must traverse the graph to get the Vault path. The execution environment then securely fetches the secret at runtime.
# output/provision_script.toml
origin_resource = "server"
[[output]]
resource_type = "provision_script"
name = "provision-{{ origin_resource.name }}"
filename = "provision-{{ origin_resource.name }}.sh"
mimetype = "text/x-shellscript"
template = '''
#!/usr/bin/env bash
set -euo pipefail
# Dynamically inject all required secrets via HashiCorp Vault
{%- for secret in origin_resource.USES_SECRET | default(value=[]) %}
{%- set path = secret.STORED_IN[0]._relation.path %}
{%- set var_name = secret._relation.injected_as %}
{{ var_name }}=$(vault kv get -field=ciphertext {{ path }})
echo "${{ var_name }}" > /opt/{{ origin_resource.name }}/{{ var_name | lower }}.key
{%- endfor %}
'''
Rescile Vault and Pre-Provisioning
In general, vaults are secure persistence boundaries for sensitive cryptographic materials, enforcing access control, auditing, and encryption at rest. While standard third-party vaults are excellent for runtime storage, bootstrapping access to them securely often requires a “vault for the vault.”
The Rescile Vault is an End-to-End Encrypted (E2EE) secret management solution tailored for this exact challenge. It can be used for everyday secrets (like database credentials or application tokens), but it particularly excels at pre-provisioning and securely sharing the master keys needed to unlock third-party and cloud vaults during automated, unattended deployments.
Architecture
The rescile-vault binary acts strictly as a client component. The vault’s server component—which manages encrypted blobs, state, and collections without ever having access to plaintext keys—is bundled natively into rescile-ce, rescile-portal, rescile-controller, and is also available as a standalone server.
For highly secure environments, the Rescile hardware appliance includes a dedicated hardware HSM to physically protect the server’s root of trust, session secrets, and JWT signing keys.
Pre-Provisioning and Pre-Seeding
A major challenge in zero-knowledge E2EE systems is granting access to systems that do not exist yet (e.g., a database server that is about to be provisioned). Rescile Vault solves this using a Zero-Knowledge Invite Token Pattern. Administrators or CI/CD pipelines can create Collections, pre-seed them with secrets, and generate single-use invite tokens for client nodes before they boot.
Built-in Vault Synchronizer
By leveraging Rescile’s graph environment, the built-in Vault Synchronizer automatically handles this entire pre-seeding and sharing process based on your infrastructure graph.
Declarative Vault Synchronization
Instead of using provision scripts to manually populate the vault, Rescile utilizes a State Reconciliation Pattern driven by the built-in Vault Synchronizer:
- Desired State (The Graph): The graph acts as the absolute source of truth for the vault’s topology (expected secrets, clients, and their relationships).
- The Synchronizer: A native component calculates the diff between the Graph and the Vault, automatically applying CRUD operations.
How Graph Topology Maps to the Vault:
The Synchronizer automatically parses your modeled graph to determine the required Vault state. It identifies target resources based on the following specific properties and relationships:
- Identified Vaults: The synchronizer targets any node with the label
vaultwhere thenameproperty contains"rescile"or theproviderproperty is set to"rescile". - Identified Secrets: Any node with the label
secretthat is connected to an identified vault via aSTORED_INrelation is managed by the synchronizer. - Identified Clients: Any node connected to a managed secret via a
USES_SECRETrelation is registered as a Vault client. The client’s identity name is resolved by checking the node’s properties in this order:vault_client_id,email, and finallyname. - Secret Naming: The name of the secret inside the Vault collection is defined by the
injected_asproperty on theUSES_SECRETrelation (defaults to"UNKNOWN"). - Roles: The client’s access role is determined by the
roleproperty on theUSES_SECRETrelation (e.g.,"Owner","Reader", defaulting to"Member"). - Collections: Collections act as isolated namespaces. The assigned collection is determined by the
collectionproperty on theUSES_SECRETrelation. If omitted, the synchronizer automatically derives a dynamic access group (collection) based on the unique set of clients sharing that specific secret.
Internal Vault Owner and Authentication:
To orchestrate these operations programmatically without exposing plaintext keys, the Vault Synchronizer acts as an internal, highly-privileged client.
- Internal Vault Owner Name: The synchronizer authenticates as a built-in administrative client named
rescile-controller. - Vault Password: The
rescile-controllerclient authenticates and encrypts its private keys using a master password. This password must be provided via theRESCILE_VAULT_PASSWORDenvironment variable. If it is not set, it securely defaults to"auto-admin-password".
If a secret cipher is missing, it is automatically generated and securely stored. Client identities are automatically registered, and their single-use invite tokens are kept ready for the target machines to claim.
Vault Synchronization using generated output templates
By leveraging Rescile’s graph environment, you can dynamically generate output templates that script this entire pre-seeding and sharing process based on your infrastructure graph.
Example: Graph-Driven Vault Provisioning
The following TOML output model loops over a vault collection defined in the graph, pre-seeds its secrets, and generates invite tokens for all connected client nodes. The script utilizes the client CLI to orchestrate the backend operations.
# output/provision_vault.toml
origin_resource = "vault"
[[output]]
match_on = [{ property = "provider", value = "rescile" }]
resource_type = "provision_script"
name = "seed-vault-{{ origin_resource.name }}"
filename = "seed-{{ origin_resource.name }}.sh"
mimetype = "text/x-shellscript"
template = '''
#!/usr/bin/env bash
set -euo pipefail
# 1. Admin authenticates (Authentication material injected via pipeline)
export RESCILE_VAULT_CLIENTNAME="admin-orchestrator"
export RESCILE_VAULT_PASSWORD="${ADMIN_VAULT_PASSWORD}"
# 2. Create the collection
rescile-vault collection create "{{ origin_resource.name }}"
# 3. Pre-seed secrets into the collection based on graph relations
{%- for secret in origin_resource.CONTAINS_SECRET | default(value=[]) %}
rescile-vault secret put "{{ origin_resource.name }}" "{{ secret.name }}"
{%- endfor %}
# 4. Pre-provision clients and share access via invite tokens (e.g., valid for 1h)
{%- for client in origin_resource.ACCESSED_BY | default(value=[]) %}
TOKEN=$(rescile-vault collection invite "{{ origin_resource.name }}" --client "{{ client.name }}" --validity 1h)
echo "export RESCILE_VAULT_INVITE_TOKEN=$TOKEN" > /tmp/invite_{{ client.name }}.env
{%- endfor %}
'''
Strict Role-Based Access Control (RBAC)
Rescile Vault enforces a rigid Role-Based Access Control (RBAC) matrix for collections. Clients hold one of three roles:
- Owner: Can read, mutate, and delete secrets. Can invite other users, revoke invites, change roles, and delete the collection. A collection must always maintain at least one Owner.
- Member: Can read and mutate secrets. This is the default role granted when a client claims an invite.
- Reader: Can only read secrets.
Orchestrators or Vault Administrators can adjust a client’s role using the CLI after they have claimed an invite:
# Demote a client to Reader after they have claimed access
rescile-vault collection role "enterprise-vault" --client "db-server-01" --role "Reader"
Consuming Pre-Provisioned Secrets (Multi-Consumer)
A core capability of Rescile Vault is the ability to securely share secrets across multiple consumers (e.g., M2M systems) using Collection Keys and Asymmetric Per-Client Keys. Because Rescile Vault decouples authentication from decryption, it scales elegantly—allowing an arbitrary number of consumers to claim access without ever sharing a master password or re-encrypting the underlying secrets.
By default, users claiming an invite receive the Member role, granting them read and write access. Vault owners can restrict access by demoting clients to the Reader role as described above.
Once the collections are automatically seeded and invite tokens are generated by the synchronizer, multiple clients can securely claim access to their respective collections during their automated boot or deployment sequence.
Below is an example showing how two distinct consumers (a Database and an Application) utilize their respective zero-knowledge invite tokens to fetch a shared pre-seeded secret.
1. Database Consumer Provisioning
The database server consumes its invite token to claim access to the collection, automatically generating its local asymmetric keypair on the first run, and retrieving the pre-seeded DB_PASS.
# output/provision_db.toml
origin_resource = "server"
match_properties = { role = "database" }
[[output]]
resource_type = "provision_script"
name = "init-db-{{ origin_resource.name }}"
filename = "init-db-{{ origin_resource.name }}.sh"
mimetype = "text/x-shellscript"
template = '''
#!/usr/bin/env bash
set -euo pipefail
# Injected by the orchestrator (from the seed script)
export RESCILE_VAULT_INVITE_TOKEN="${INVITE_TOKEN_DB}"
export RESCILE_VAULT_CLIENTNAME="{{ origin_resource.name }}"
export RESCILE_VAULT_PASSWORD=$(openssl rand -base64 32) # Ephemeral local password
# Claim the invite and retrieve the pre-seeded secret
DB_PASS=$(rescile-vault secret get "{{ origin_resource.name }}" "DB_PASS")
# Apply to local database...
echo "ALTER USER postgres PASSWORD '${DB_PASS}';" | psql -U postgres
'''
2. Application Consumer Provisioning
Similarly, the application server consumes its unique invite token. The client CLI automatically unwraps the Collection Key from the invite, securely encrypts it against the application’s newly generated public key, and fetches the exact same DB_PASS.
In this example the invite token is fetched from the vault.
# output/provision_app.toml
origin_resource = "server"
match_on = [{ property = "role", value = "application" }]
[[output]]
resource_type = "provision_script"
name = "init-app-{{ origin_resource.name }}"
filename = "init-app-{{ origin_resource.name }}.sh"
mimetype = "text/x-shellscript"
template = '''
#!/usr/bin/env bash
set -euo pipefail
# Fetched from the Vault (Automatically generated by the Synchronizer)
export RESCILE_VAULT_INVITE_TOKEN=$(curl -s http://localhost:7600/vault/v1/invite/{{ origin_resource.name }})
export RESCILE_VAULT_CLIENTNAME="{{ origin_resource.name }}"
export RESCILE_VAULT_PASSWORD=$(openssl rand -base64 32) # Ephemeral local password
# Claim the invite and retrieve the shared pre-seeded secret
DB_PASS=$(rescile-vault secret get "{{ origin_resource.name }}" "DB_PASS")
# Write to application configuration
cat <<EOF > /etc/myapp/config.yml
database:
host: db-server-01
password: ${DB_PASS}
EOF
'''