Skip to main content

How It Works

The OneDrive connector is a self-contained integration that runs entirely inside your Azure subscription. ROOTKey publishes a Terraform module that you deploy once. The module wires up an Azure Function App that subscribes to Microsoft Graph webhook notifications for one OneDrive drive. When the drive changes, the function runs a Graph delta query to identify the new/updated files and streams each file to the ROOTKey API authenticated with your Connector API Key. ROOTKey stores the file and anchors it on-chain, enabling both integrity verification and full file recovery in the event of corruption, ransomware, or accidental deletion.
ROOTKey’s cyber resilience guarantee includes full recovery — not just detection. For that reason the connector uploads the full file content, not only a hash. Anchoring a hash alone cannot restore a corrupted, encrypted, or deleted file.
The connector is bound to one drive per deployment — the Graph drive ID is part of the module’s input. To monitor multiple drives, deploy the module once per drive; the resources are namespaced by a configurable name_suffix so they coexist in the same resource group.

What This Module Creates in Your Azure Subscription

Full transparency on what lands in your subscription when you terraform apply. Everything is namespaced by name_suffix + a deterministic hash of the drive ID, so multiple deployments don’t collide. The module does not create or modify the Microsoft Entra ID App Registration — you create that yourself and pass the Tenant ID, Client ID, and Client Secret in (see Prerequisites). It also does not create the Resource Group, which must pre-exist. For a typical drive with a few thousand uploads per month, the total recurring cost added to your Azure bill is well under $5/month, dominated by egress to the ROOTKey API.

Infrastructure Impact Summary

Apart from creating one Microsoft Graph webhook subscription on the configured drive, no. The connector reads files via the App Registration’s Files.Read.All permission and never writes back to OneDrive. No mailbox, SharePoint site, or Teams resource is touched.
The module creates new resources inside the Resource Group you specify and does not modify any pre-existing resources in it. Role assignments are scoped to the resources the module itself creates — it does not grant any permissions on resources outside its scope.
Yes. The whole purpose of the connector is to forward file content to ROOTKey so it can be anchored and recovered. Transport is HTTPS-only (the module rejects non-https:// API URLs at plan time). Files are streamed directly from Graph to the ROOTKey API; the Function App never writes them to local storage or to any other Azure service.
The Graph client secret you paste into Terraform, the ROOTKey API Key, and a randomly generated webhook clientState are all stored in Azure Key Vault in your own subscription, encrypted at rest with the Microsoft-managed key for Key Vault. The Function App resolves them at boot using its managed identity and Key Vault references (@Microsoft.KeyVault(SecretUri=…)). They are not stored as plain Function App settings.
Each file gets up to 3 upload attempts with exponential backoff (initial 1s, capped at 30s) before being sent to the SQS-equivalent Azure Storage DLQ. From the DLQ, a queue-triggered function automatically replays each message up to 5 more times (queue retries with backoff). Only after all those retries fail does the message land in the rootkey-dlq-poison queue for human inspection. You can configure an Azure Monitor alarm on the DLQ or poison queue length to be notified.
A timer trigger runs every 12h (and on every cold start, via runOnStartup) that does two things: renews/recreates the Graph subscription, and runs a safety-net delta sync. So even if a notification is dropped, the missed changes are picked up — at worst, within the next 12h, or immediately on the next deploy/restart.
The webhook is protected by a 32-char random clientState value generated at apply time and stored in Key Vault. Any POST to the webhook URL without the matching clientState is rejected with 401. This prevents random or malicious callers from triggering work.
Yes. Running terraform destroy removes every resource the module created (Function App, storage account, Key Vault, App Insights, identity, role assignments). The App Registration and the Resource Group are not deleted — they are your resources, not the module’s. Note: if you enabled purge_protection on the Key Vault (the default), the Vault and its secrets will remain in soft-delete state for 7 days after destroy before they can be fully purged.

Prerequisites

Before starting, ensure you have:
  • An Azure subscription and a pre-existing Resource Group to host the connector.
  • Permissions to register applications in Microsoft Entra ID (Application Administrator or Global Administrator) and to grant admin consent.
  • Permissions to apply Terraform with Contributor and User Access Administrator (or equivalent) on the chosen Resource Group.
  • EventBridge-equivalent: nothing extra on the OneDrive side — the connector self-registers the Graph subscription. You only need to confirm the drive ID.
  • Terraform v1.11 or later installed locally (or in a CI/CD pipeline that runs terraform apply). This is a hard floor, not a recommendation: the module uses write-only arguments to keep your secrets out of the Terraform state file, and those require 1.11. An older version fails at terraform init with an explicit version error rather than silently writing your secret to disk.
  • Node.js v22 or later on the machine running Terraform — the Function App source is compiled at apply time.
  • Azure CLI authenticated (az login) or service principal credentials in the environment.

Required Microsoft Graph and Azure Permissions

Microsoft Graph (Application permission)

The App Registration you create needs one Microsoft Graph application permission with admin consent:

Azure RBAC (granted by the module to its own managed identity)

For full transparency — the module attaches these role assignments to a brand-new user-assigned managed identity it creates. None of these grant access to anything outside the resources the module itself provisions: The Terraform principal applying the module needs Contributor (to create the resources) and User Access Administrator (to attach those role assignments) on the Resource Group.

Configuration Fields


Setup

The setup has a natural ordering: the dashboard requires the Tenant/Client/Drive IDs to create the connector, and the Function App requires the Connector API Key to call the ROOTKey API. The dashboard resolves this by generating a ready-to-run Terraform block with all values pre-filled.
1

Register an application in Microsoft Entra ID

Go to the Azure Portal → Microsoft Entra ID → App registrations → New registration.
  • Name: something descriptive, e.g., ROOTKey OneDrive Connector.
  • Supported account types: Accounts in this organizational directory only.
  • Redirect URI: leave blank.
Click Register. Note the Application (client) ID and Directory (tenant) ID — you will need both.
2

Grant Microsoft Graph permission

In your new App Registration, go to API permissions → Add a permission → Microsoft Graph → Application permissions.Add Files.Read.All, then click Grant admin consent for [your tenant] and confirm.
3

Create a client secret

Go to Certificates & secrets → New client secret.
  • Set an expiry appropriate for your rotation policy (e.g., 12 or 24 months).
  • Click Add and immediately copy the Value — it is shown only once.
Store the secret securely until you paste it into the dashboard.
Azure App Registration secrets expire. Set a calendar reminder ahead of the expiry — when the secret expires, the connector starts failing with 401 from Graph. Rotation steps are in the Troubleshooting section.
4

Find the Drive ID

Retrieve the drive ID via Microsoft Graph Explorer or directly with the Graph API. For a specific user:
Copy the id of the drive you want to monitor.
5

Pre-create the Resource Group

In your subscription, create (or pick) a Resource Group to host the connector. The Terraform principal needs Contributor and User Access Administrator on that Resource Group.
6

Create the connector in the dashboard

Go to app.rootkey.ai → Connectors → New Connector → select OneDrive.Fill in all required fields (see Configuration Fields above). Save the connector.
7

Copy the Connector API Key and the Terraform block

At the end of the wizard, the dashboard displays:
  1. The Connector API Key.
  2. A ready-to-run Terraform block, pre-filled with your values.
The Connector API Key is shown only once and is already embedded in the Terraform block. Copy both now and store them securely before closing this screen. The key cannot be retrieved again.
The generated block looks like:
8

Deploy the Terraform module

Save the block into a .tf file in an empty directory, then run:
The module bundles the Function App source, provisions every resource, and wires the managed identity, Key Vault, and storage roles.
9

Validate the connector

Allow 20–30 minutes before your first test. The timer is registered with runOnStartup: true, but on the Flex Consumption plan Azure runs each trigger type on its own instance group, and the group that owns the timer is not provisioned the moment the app is created. In a measured deployment the gap between terraform apply finishing and the first run was 19 minutes. During that window the Function App is healthy and answering HTTP, and no subscription exists yet — a file uploaded then is picked up by the next delta sync, not lost. You can confirm by inspecting the connector-state blob container:
You should see subscription.json (and, after the first sync, delta-link.txt).Then upload a test file to the monitored drive. Within seconds it should appear in the destination vault and the connector status in the dashboard should be ACTIVE.

Reliability and observability

The connector is built for at-least-once delivery to ROOTKey with explicit handling of every failure mode.

Retry behaviour

Idempotency

Every upload to the ROOTKey API carries three headers extracted from the Graph drive item: The ROOTKey API uses these to deduplicate redelivered events.

Concurrent invocations

Only one delta sync per drive runs at a time. The connector acquires a blob lease on delta-sync.lock in the connector-state container before running. Concurrent webhook invocations on the same drive return 202 Accepted immediately and let the holder finish. The lease auto-expires after 60 s if the holder crashes, so the system self-recovers.

What to monitor

A starting Kusto query to see recent errors in App Insights:
To peek at DLQ contents:

Secrets and the Terraform state file

Terraform records the attributes of everything it manages in a state file. By default that includes the value of any secret passed into a module — marking a variable sensitive only masks it in command output, it does not keep it off disk. In a regulated environment this is usually the first question asked about any infrastructure-as-code module, so it is worth being precise about what this one does.

Your secrets are never written to state

Both the Microsoft Graph client secret and your ROOTKey API Key are written to Key Vault using value_wo, a write-only argument. The provider receives the value, sends it to Azure Key Vault, and Terraform persists nothing. The value is equally absent from a saved plan file (terraform plan -out=…), because the corresponding input variables are declared ephemeral — worth knowing if you run the module from a CI/CD pipeline, where a saved plan is exactly the kind of thing that gets archived as a build artifact. You can verify this yourself after an apply. The following should return 0:

The trade-off: Terraform cannot detect that a secret changed

It never sees the value, so it has nothing to compare against. That is what graph_client_secret_version and rootkey_api_key_version are for.
Change a secret and increment its counter, and the new value is written. Change a secret and leave the counter alone, and the apply succeeds while silently doing nothing. This is the one sharp edge of the design — the rotation procedures always name both steps for this reason.

What does remain in the state file

Being complete about this matters more than the headline: This list was produced by walking the Terraform provider schema for every resource the module creates, taking each attribute marked sensitive, and checking it against a real applied state — not written from memory. You can reproduce it yourself; see below.
None of these grants access to your Microsoft 365 tenant, to SharePoint, or to ROOTKey — they are scoped to the resources this module created. The one to treat as a real credential is the SCM publishing password: whoever holds it can deploy code to the Function App. If your policy does not allow that in a state file, set webdeploy_publish_basic_authentication_enabled = false on the Function App and it becomes inert, the same way the storage keys already are.
Reproduce this against your own deployment. Terraform marks every sensitive attribute itself:
Open state.json and read each resource’s sensitive_values block: every attribute set to true is one Terraform considers sensitive, and the matching entry under values is what was actually recorded. On a real deployment of this module that yields 19 entries — and the two that matter read like this:
Empty, because the module writes them with a write-only argument.

Where to keep the state file

Even with no secrets in it, the state is an accurate map of your deployment and should not live on an operator’s laptop. Use a remote backend in your own cloud account — it also gives you state locking, so two people cannot apply at the same time:
The trust boundary is the one you already accepted when you let the module create Azure Key Vault inside your own account. If that is acceptable, the state file is acceptable in the same place.

Security considerations

The module ships with a defensive default posture; a few choices have intentional trade-offs that are worth understanding upfront:
  • Secrets in Key Vault, not Function App settings. Graph client secret, ROOTKey API key, and webhook clientState are all stored in Key Vault with the Function App’s managed identity granted Key Vault Secrets User (read-only) RBAC.
  • Key Vault purge protection is enabled by default. Set enable_key_vault_purge_protection = false only for short pilots — once enabled it CANNOT be disabled and the vault cannot be fully purged for 7 days after terraform destroy.
  • CORS is closed. The webhook is server-to-server (Graph); browser access is explicitly disallowed.
  • HTTPS-only, TLS 1.2 minimum, FTPS disabled on the Function App. HTTP/2 enabled.
  • Storage Account shared access keys are disabled (shared_access_key_enabled = false). Every access — the Function App’s deployment bundle, its own state blobs, and the dead-letter queue — goes through the user-assigned managed identity with RBAC, and AzureWebJobsStorage is wired identity-based via AzureWebJobsStorage__accountName. There are no storage keys to leak, rotate, or record in the Terraform state. This is one of the reasons the connector runs on the Flex Consumption plan; the older Linux Consumption plan required the legacy connection string and could not turn the keys off.

Filtering Rules

To anchor only specific files (e.g., only PDFs, or exclude temporary files), configure Filtering Rules on the connector after creation. Rules apply on the ROOTKey side — files filtered out are not stored in the vault.

Troubleshooting

Check in this order:
  1. The webhook subscription exists. Inspect the subscription.json blob in the connector-state container. If absent, force the timer to run via the Azure Portal (Function App → renewSubscription → Code + Test → Run).
  2. The DLQ. Use az storage message peek on rootkey-dlq. If messages are present, look at rootkey-dlq-poison too — that’s where messages land after all replays fail.
  3. App Insights traces. Run the Kusto query in Reliability and observability to find recent errors.
  4. Graph permissions. In Microsoft Entra ID → App registrations → API permissions, Files.Read.All must be granted with admin consent (green check next to it).
Common causes:
  • Client Secret has expired. Generate a new secret in the App Registration, update the graph_client_secret Terraform variable, and run terraform apply. The new secret is written to Key Vault and the Function App picks it up on the next cold start (or restart the Function App to force it).
  • Admin consent was revoked for Files.Read.All. Re-grant consent in the App Registration.
  • The destination ROOTKey vault was deactivated or deleted. Reactivate it or change the connector’s vault binding.
The dashboard error panel shows the underlying message from the ROOTKey API or the Graph service.
The Function App’s memory and max_file_size_bytes together cap the maximum file size. The default is 500 MiB on a Consumption plan instance.To support larger files: raise max_file_size_bytes in the Terraform module input and consider moving to a Premium or App Service plan with a higher memory ceiling. Open an issue on the connector repository if you need help.
  1. In the dashboard, delete the connector and create a new one (the App Registration and Drive ID can be reused).
  2. Update the rootkey_api_key Terraform variable with the new key.
  3. Run terraform apply — the module writes the new key into Key Vault. The Function App picks it up on the next cold start.
  1. In Microsoft Entra ID → App registrations → your app → Certificates & secrets, create a new client secret. Copy its value immediately.
  2. Update the graph_client_secret Terraform variable.
  3. Run terraform apply — the new value goes into Key Vault.
  4. Restart the Function App (e.g., az functionapp restart) to force it to pick up the new secret immediately, instead of waiting for the cached OAuth token to expire (up to 1 h).
  5. After confirming the connector is healthy, delete the old secret in the App Registration.
Yes — deploy the module once per drive. Each instance is fully isolated: its own Function App, Key Vault, storage account, DLQ, and identity, namespaced by name_suffix and a hash of the drive ID. You can reuse the same App Registration and Resource Group across drives.
The poison queue holds messages that the queue trigger could not process after all retries. Inspect each message — it includes the original DLQ payload (driveId, itemId, fileName, size, eTag, error). Common root causes:
  • The drive item was deleted or moved by the user before retries could complete (safe — the message can be discarded).
  • The ROOTKey vault is unreachable due to a misconfiguration or a key/vault rotation gone wrong.
  • A persistent Graph permission issue.
Once the root cause is resolved you can replay a message by copying it back to rootkey-dlq (Azure Storage Explorer makes this easy).
With enable_key_vault_purge_protection = true (the default), Azure prevents the Key Vault from being fully deleted until the soft-delete retention window (7 days) elapses. If you destroy and re-apply within that window, Terraform may attempt to recover the soft-deleted Key Vault automatically (the module’s provider config enables recover_soft_deleted_key_vaults). For short-lived pilots that need to recycle freely, set enable_key_vault_purge_protection = false.

Source code

The Terraform module and Function App source live in the public ROOT-Key/rootkey-connectors repository under the onedrive/ directory. The code is licensed under the Apache License 2.0 — you are free to fork it, audit it, or pin to a specific commit if your change-management process requires it.
→ Back to Connectors Overview