Chio/Docs
LOGIN · JOIN

Decoys and tripwires

Recognize contact with tenant-bound decoys and signed watermarks while keeping artifact custody, lifecycle, and observation confidence explicit.

Concept chio@5f63e6e

On this page

Plant a credential no legitimate task should use, and the first attempt to use it is refused and recorded. A tripwire is a controlled artifact whose contact traces back to a registered owner and tenant, so a touch tells the operator which decoy, on which surface, and whether it was read or presented as authority.

A repository indexer reading a decoy and a caller presenting a canary as execution authority establish different facts. The private registry keeps each artifact's lifecycle and the materializer keeps the identity of the file it created, so later contact can be interpreted and the exact object retired after an interruption or replacement.

Decoy contactChoose a contact.
aindexerreadsa′callerpresentsdetectρevidencedeny: truedexecutorfdecoygdetectortenant, surface, lifecycleaindexerreadsa′callerpresentsρevidencedeny: truedexecutorfdecoygdetectorregistry check

Presentation as authority is refused before any executor

A caller presents the decoy as a credential. The detector returns ActiveMatch with requires_immediate_deny: true, so the protected invocation refuses it, and the contact is still recorded as evidence.

Contact with one credential-file decoy, in the four detections the disposable exercise compares. Event signing, ingress, and correlation that follow detection are left out.

Placement decides what a contact means

A decoy in a directory the application indexes every hour produces different evidence from a canary presented as an execution credential, even when the marker is authentic in both cases. Before arming one, name the work that will brush against it: scanners, backup jobs, reviewers. That context keeps ordinary indexing from reading as credential theft and gives detector rules a basis for telling contact from attempted use.

Take a credential-shaped file in a tenant's repository area that a legitimate indexing job searches, kept apart from live provider secrets. The indexer touching it is a benign control: it establishes contact and nothing about intent. Presenting a canary capability for execution is a different action, and it never reaches an effectful executor. A placement review settles six points:

  1. Encounter. Which search, indexing, backup, and diagnostic workflows can reach it.
  2. Significance. Which action makes contact meaningful: seeing the content, or presenting it for use.
  3. Custody. Which service owns the materialized artifact, and what file-ownership evidence it retains.
  4. Expiry. When the artifact stops being active, and through which lifecycle transition.
  5. Inspection. The event, registry reference, source context, and producer identity the operator will read.
  6. Exclusion. The canary route reaches no effectful executor and no live credential.

Every decoy also needs an operational owner who provisions it, keeps its registry binding, rotates or expires it, and removes the exact object at retirement. A forgotten decoy produces misleading observations later, and careless cleanup can delete a file another actor placed at the same path. Keep the private registry and its administration outside ordinary content access, use separate development and production identities, and record any privileged export that exposes a marker.

Each decoy surface answers a different question

The registry binds each observation to a tenant, a surface, a lifecycle state, and an evidence record. DecoySurface names eight surfaces.

crates/security/chio-security-types/src/deception.rs13–22rust
pub enum DecoySurface {
    CanaryCapability,
    HoneyTool,
    CredentialArtifact,
    CredentialFile,
    FileMarker,
    BrowserCookie,
    InternalHostname,
    SignedWatermark,
}
SurfaceUseful observationCustody concern
Canary capability or honey toolAn attempt to use an unauthorized routeThe decoy never reaches an effectful tool
Credential artifact or fileContact with planted credential materialKept apart from live provider secrets
File marker or browser cookieMovement through a specific data surfaceTenant and materialization ownership preserved
Signed watermarkA verified appearance of marked contentIssuer, source context, sequence, and replay checked

A signed watermark binds marked content to an issuer key, a source context, and a sequence number, and a later appearance is checked by signature, never by string match. Keep raw decoy material out of ordinary telemetry and public receipts: evidence references and commitments serve routine correlation, and privileged export of registry material sits behind its own authorization.

Presentation as authority is refused; observed contact becomes evidence

An active direct presentation requires immediate denial. An embedded marker that is merely observed produces evidence without the same blocking, so a scanner, a quoted document, or a diagnostic log can touch a decoy without being treated as an attempt to use it as authority. Every match reports malice_proven: false.

The resulting security event passes producer verification before it can feed automatic response; a string that resembles a canary authenticates nothing about its registry record. To investigate a contact, verify the producer and the tenant partition, resolve the artifact through authorized registry access, and check its lifecycle at the moment of observation. Then preserve the source receipt and compare the contact with the legitimate task, including the benign control.

Lifecycle state decides whether a contact is live

The private registry resolves markers within the tenant and surface, and the detector reports an active match only when the record is matchable and unexpired. Six explicit states cover planned material, materialization, active use, triggering, rotation, and retirement.

crates/security/chio-security-types/src/deception.rs77–84rust
pub enum DecoyLifecycleState {
    Planned,
    Materializing,
    Armed,
    Triggered,
    Rotating,
    Retired,
}

An expired or inactive artifact turns contact into an inactive observation. Registry unavailability, integrity failures, and lifecycle errors are failures to establish the observation, retained as failures and never as clean negatives. Retirement changes how later contact is read and leaves observations authenticated before it in place.

Show the retained state and operation identity when diagnosing a marker. Planned says nothing yet about a file on disk. A Materializing record after a crash needs reconciliation against the exact owned filesystem object, because materialize_file commits the registry to Materializing before it calls the file materializer: a registry transition alone shows no external file operation happened exactly once. Test an interruption between each lifecycle operation and its durable result.

A disposable credential file separates indexing, presentation, and expiry

The exercise creates dummy material, places it through the materializer, reads the file back, and compares benign indexing, direct presentation, and presentation at expiry, keeping the materialization receipt that exact-object cleanup needs. Download decoy_contact.rs.

The complete materialization, contact, and cleanup integration

Supply a disposable private PrivateDecoyRegistry, its durable store and key provider, and a FileMaterializer opened on the same empty private directory passed as root. The tenant and operation IDs belong to the trusted test host. The marker is dummy data with no provider authority, the scope and creation-policy names are fixture choices, and the registry is separate from production decoys.

security-host/decoy_contact.rsrust
use chio_decoy::{
    DecoyCreateRequest, DecoyDetection, DecoyDetector, FileMaterializer, MaterializationReceipt,
    ObservationClass, PrivateDecoyRegistry, SecretMaterial, TripwireObservation,
};
use chio_security_types::ports::{ArtifactId, RecordId, TenantId};
use chio_security_types::{DecoyOperationAttempt, DecoyRecord, DecoySurface, DecoyVersion};
use std::{path::Path, sync::Arc};

pub struct ContactComparison {
    pub materialization: MaterializationReceipt,
    pub scanner: DecoyDetection,
    pub direct: DecoyDetection,
    pub expired: DecoyDetection,
}

/// Only a disposable private registry and materializer root. The dummy bytes
/// are neither a provider credential nor permission to use a real endpoint.
/// Retain the same artifact/create/materialization IDs on an interrupted retry.
pub fn place_and_compare(
    registry: Arc<PrivateDecoyRegistry>,
    materializer: &FileMaterializer,
    root: &Path,
    tenant: TenantId,
    artifact: ArtifactId,
    create_operation: RecordId,
    materialize_operation: RecordId,
    now_unix_ms: u64,
) -> Result<ContactComparison, Box<dyn std::error::Error>> {
    let expires = now_unix_ms.checked_add(60_000).ok_or("invalid expiry")?;
    let marker = b"CHIO-DUMMY-CREDENTIAL-FILE-REVIEW-ONLY";
    registry.create(
        DecoyCreateRequest {
            tenant_id: tenant.clone(),
            artifact_id: artifact.clone(),
            surface: DecoySurface::CredentialFile,
            scope_id: RecordId::new("pr-421-review")?,
            creation_policy_id: RecordId::new("disposable-review-policy")?,
            version: DecoyVersion::new(1)?,
            expires_at_unix_ms: expires,
            predecessor_artifact_id: None,
            marker: SecretMaterial::new(marker.to_vec())?,
            materialization_payload: Some(SecretMaterial::new(marker.to_vec())?),
        },
        create_operation,
    )?;
    let relative = Path::new("review-decoy.txt");
    let materialization = registry.materialize_file(
        &tenant,
        &artifact,
        &materialize_operation,
        relative,
        materializer,
        now_unix_ms,
    )?;
    let presented = std::fs::read(root.join(relative))?;
    if presented != marker {
        return Err("materialized bytes differ from dummy marker".into());
    }
    let detector = DecoyDetector::new(registry);
    let observe = |class, time| {
        detector.detect(&TripwireObservation {
            tenant_id: &tenant,
            surface: DecoySurface::CredentialFile,
            presented: &presented,
            class,
            observed_at_unix_ms: time,
        })
    };
    let scanner = observe(ObservationClass::InventoryScanner, now_unix_ms)?;
    let direct = observe(ObservationClass::DirectPresentation, now_unix_ms)?;
    let expired = observe(ObservationClass::DirectPresentation, expires)?;
    assert!(matches!(
        &scanner,
        DecoyDetection::ActiveMatch {
            malice_proven: false,
            requires_immediate_deny: false,
            ..
        }
    ));
    assert!(matches!(
        &direct,
        DecoyDetection::ActiveMatch {
            malice_proven: false,
            requires_immediate_deny: true,
            ..
        }
    ));
    assert!(matches!(
        &expired,
        DecoyDetection::InactiveObservation { expired: true, .. }
    ));
    Ok(ContactComparison {
        materialization,
        scanner,
        direct,
        expired,
    })
}

/// The host has already retained the rotation and armed replacement. The
/// original Retire attempt binds generation/version and the exact file receipt.
/// A replacement at the old path must not be unlinked to force successful cleanup.
pub fn retire_original_file(
    registry: &PrivateDecoyRegistry,
    materializer: &FileMaterializer,
    tenant: &TenantId,
    artifact: &ArtifactId,
    original_retire: &DecoyOperationAttempt,
) -> Result<DecoyRecord, chio_decoy::RegistryError> {
    registry.retire_materialized_file(tenant, artifact, original_retire, materializer)
}

You should see the function return without a failed assertion: it checks the return variants itself and prints nothing. The scanner's contact is ActiveMatch with requires_immediate_deny: false, direct presentation is ActiveMatch with requires_immediate_deny: true, and the same presentation at expiry is InactiveObservation with expired: true. A registry or lifecycle failure would return DetectionFailure; hold the protected path and repair its registry owner.

The comparison classifies contact and stops there. In production the tripwire guard and hook publish a context-bound signed event through authenticated ingress, and response authority decides what follows.

Cleanup removes the exact object the service created

Materialized files carry ownership evidence. Cleanup refuses a file whose ownership proof or content digest differs from what the service created, and a lifecycle retry presents the original recorded operation identity, so recovery never replaces another actor's file.

Keep the original artifact, creation, and materialization operation IDs if the host stops. Registry state may have advanced even though the caller lost the acknowledgment; resuming the same operation recovers its receipt, while a new ID or a delete by path loses the binding. During rotation, arm and verify the recorded replacement before retiring its predecessor. retire_original_file uses the original retained Retire attempt, and if another file now occupies the old path, cleanup refuses that object; preserve the materialization proof and inspect the registry's retained failure before repairing or retrying the retirement.

Before arming a production cohort, exercise a known active marker, an expired marker, and a marker from another tenant, and confirm the four outcomes stay distinct: clear, active match, inactive observation, and registry failure. Confirm that a canary invocation reaches no tool executor, stubbed or live. Remove development markers and keys, review the legitimate jobs that touch the surface, and keep false-positive analysis with temporal detection and rollout evidence.

Rehearse replacement and cleanup races

After creating a controlled file, replace its pathname in a test with another actor's file before cleanup runs, and check that cleanup recognizes the ownership mismatch and leaves the replacement in place. Repeat cleanup under the original lifecycle identity to check that retries stay tied to the original artifact.

Pair the race with a benign workload that searches the containing directory. The detector should keep contact and attempted use apart, and the response policy should follow that distinction. Retain the benign control beside the attack case, so a later change cannot improve apparent detection by blocking normal work.

Next: authenticate the observation a tripwire produces