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"
[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 %}
'''