Skip to Content
KnowaKYC
Admin panel

Tenant storage (BYOB)

Status: Generally available since 2026-04-22.

The Storage section lets a tenant connect its own object storage and host applicant documents, KYB materials, and selfies there instead of in KnowaKYC's shared storage. Supported providers are S3, S3-compatible buckets, GCS, and Azure Blob containers. This capability is called BYOB — bring-your-own-bucket.

By default, every tenant uses KnowaKYC's shared storage (mode shared). Connecting a tenant-owned bucket is an opt-in feature, available without data migration and without downtime.

When to use BYOB

Connecting your own bucket makes sense in the following cases:

  • Regulatory / data-residency compliance. User documents must be stored in a specific jurisdiction or in infrastructure controlled by the tenant.
  • Corporate security policies. Data access must be limited to the tenant's security team. The tenant brings its own KMS keys, its own IAM, and its own audit trail (CloudTrail / Cloud Audit Logs / Storage Analytics).
  • Cost control. The tenant pays for storage directly to its cloud provider.
  • Greater lifecycle control. Self-managed lifecycle policies (archival → Glacier / Coldline / Cool tier, retention).

If none of these are critical, keep shared storage — it requires zero setup and is operated by the KnowaKYC team.

Supported providers

Providerprovider valueAuthenticationNotes
AWS S3aws_s3STS AssumeRole + ExternalIDCross-account role; KnowaKYC does not store long-term AWS keys
S3-compatibles3_compatStatic AccessKey + SecretKeyCloudflare R2, Backblaze B2, MinIO, Wasabi, IBM Cloud Object Storage
Google Cloud StoragegcsService Account JSONbase64-encoded Service Account key
Azure Blob Storageazure_blobAccount Key, ConnectionString, or SAS TokenAny of the three

For AWS S3, the AssumeRole + ExternalID pattern is used — AWS's recommended way to grant a third-party service limited access to account resources without long-term credentials. The ExternalID protects against the "confused deputy" attack.

Creating a configuration

Through the admin panel

  1. Go to Settings → Storage.
  2. Click "Connect own bucket".
  3. Pick the provider type and fill in the parameters (see below).
  4. Save the draft — the configuration is created in pending status and is not yet used.
  5. Run validation (see "Configuration validation" below).
  6. After validation succeeds — activate the configuration.

Through the API

Example for AWS S3

POST /v1/admin/storage/configs HTTP/1.1
Authorization: Bearer <admin_jwt>
Content-Type: application/json

{
  "provider": "aws_s3",
  "bucket": "my-tenant-onekyc",
  "region": "eu-central-1",
  "aws_role_arn": "arn:aws:iam::123456789012:role/KnowaKYCStorage",
  "sse_mode": "sse_kms",
  "sse_kms_key_id": "arn:aws:kms:eu-central-1:123456789012:key/abcd-..."
}

The response returns the configuration object in pending status and a generated aws_external_id. Add this ExternalID to the trust-policy condition of the IAM role on the tenant side (see below).

Example for an S3-compatible store (Cloudflare R2)

POST /v1/admin/storage/configs HTTP/1.1

{
  "provider": "s3_compat",
  "bucket": "tenant-onekyc-r2",
  "region": "auto",
  "endpoint": "https://<account-id>.r2.cloudflarestorage.com",
  "use_path_style": true,
  "access_key_id": "<R2 access key id>",
  "secret_key": "<R2 secret>"
}

Example for Google Cloud Storage

POST /v1/admin/storage/configs HTTP/1.1

{
  "provider": "gcs",
  "bucket": "tenant-onekyc-gcs",
  "service_account_json": "<base64-encoded SA JSON key file>"
}

Example for Azure Blob

POST /v1/admin/storage/configs HTTP/1.1

{
  "provider": "azure_blob",
  "bucket": "onekyc-data",
  "endpoint": "<storage account name>",
  "azure_account_key": "<account key>"
}

The fields secret_key, service_account_json, azure_account_key, azure_connection_string, azure_sas_token are write-only — they are encrypted and never returned by any endpoint.

Required IAM permissions (AWS)

Trust policy of the IAM role (on the tenant side):

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "AWS": "arn:aws:iam::<onekyc account id>:role/onekyc-byob-resolver" },
    "Action": "sts:AssumeRole",
    "Condition": {
      "StringEquals": { "sts:ExternalId": "<aws_external_id from Create response>" }
    }
  }]
}

Permission policy of the IAM role:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": [
      "s3:PutObject",
      "s3:GetObject",
      "s3:DeleteObject",
      "s3:GetObjectAcl",
      "s3:PutObjectAcl"
    ],
    "Resource": "arn:aws:s3:::<bucket>/*"
  }, {
    "Effect": "Allow",
    "Action": [
      "s3:ListBucket",
      "s3:GetBucketLocation"
    ],
    "Resource": "arn:aws:s3:::<bucket>"
  }]
}

If SSE-KMS mode is enabled (sse_mode = "sse_kms"), a KMS-key policy is also required:

{
  "Effect": "Allow",
  "Action": [
    "kms:Encrypt",
    "kms:Decrypt",
    "kms:GenerateDataKey"
  ],
  "Resource": "<sse_kms_key_id>"
}

Configuration validation (validation probe)

Before activation the configuration goes through 14 automated checks (10 baseline + 4 production-readiness):

POST /v1/admin/storage/configs/{id}/validate
#CheckWhat it verifies
1head_bucketThe bucket exists and s3:ListBucket is granted
2put_probeA probe object is uploaded (__probe__/health-*.txt)
3get_probeThe same object is read back
4delete_probeThe probe object is deleted
5presign_getA signed GET URL is generated (used to serve files to the frontend)
6presign_postA signed POST URL is generated (used for direct upload from the applicant's device)
7list_objectsListing objects by prefix
8category_dispatchCorrect split by category (documents / selfies / ...)
9health_quickBaseline provider health check
10probe_cleanupAll created probe objects are removed
11public_accessAnonymous read must be denied (Block Public Access)
12clock_skewServer / client clock skew is within 60 s
13multipart_uploadMultipart upload of large files works correctly
14copy_objectServer-side copy is supported (required for shared → tenant migration)

A 15th probe (sse_enforcement) runs when sse_mode = "sse_kms" and suggests a bucket policy that denies uploads without encryption.

The response contains per-probe details:

{
  "data": {
    "report": {
      "results": [
        { "name": "head_bucket", "passed": true, "duration_ns": 145000000 },
        { "name": "put_probe", "passed": false,
          "error": "AccessDenied",
          "action_hint": "Grant s3:PutObject on bucket prefix" }
      ],
      "all_passed": false
    },
    "all_passed": false
  }
}

For each failed check an action_hint is returned — a concrete suggestion ("add s3:PutObject", "remove Public Access"). Validation returns 200 OK even when some probes fail (with all_passed=false) — this lets the UI show a checklist without parsing the error envelope.

Activation

After validation succeeds, the configuration is moved to the active state:

POST /v1/admin/storage/configs/{id}/activate
X-Confirm-Tenant: <tenant UUID>

The X-Confirm-Tenant header is required — it guards against accidental activations. Its value must match the current tenant's UUID.

After activation:

  • Every new upload (documents, selfies, etc.) goes straight into the connected tenant bucket.
  • Historical data in shared storage is copied in the background.

The action is recorded in the audit log (tenant.updated + action=storage_config_activated).

Historical data migration

Activation switches new uploads to the connected bucket immediately. Files that were uploaded earlier remain available while KnowaKYC copies them to the tenant bucket.

Parameters

  • Rate limit: 10 MB/s per tenant by default.
  • Integrity check: copied objects are verified after upload.
  • Grace period: 30 days between migration completion and the deletion of copies from shared storage.

Migration job states

StatusDescription
pendingJob created, waiting to start
runningCopying in progress
pausedPaused (only via super-admin)
completedAll objects copied successfully
failedRetry limit reached; see last_error
cancelledThe tenant initiated Disable during migration

Migration status request

GET /v1/admin/storage/migration/current

The response contains total_objects / copied_objects / failed_objects counters, byte progress, started_at, completed_at, and the last error.

Health monitoring

GET /v1/admin/storage/health

Returns the latest storage health snapshot:

FieldValue
statushealthy / degraded / unhealthy
last_check_atTime of the most recent check
last_success_atTime of the most recent successful check
consecutive_failuresNumber of consecutive failed checks
last_error / last_error_codeThe latest error
alert_sent_atWhen a degradation notification was sent

Health checks run automatically in the background (a lightweight probe). After several consecutive failures, the tenant administrator receives an email notification.

Credentials rotation

If the keys are compromised or scheduled rotation is due:

POST /v1/admin/storage/configs/{id}/rotate
Content-Type: application/json

{
  "access_key_id": "<new AccessKey>",
  "secret_key": "<new SecretKey>"
}

For AWS S3, aws_role_arn is typically rotated and the ExternalID is regenerated; for GCS — a new service_account_json; for Azure — a new Account Key / SAS Token.

After rotation the status resets to pending — you have to run Validate again, and only after a successful validation does the configuration become active again (Activate).

Disable

If the tenant wants to return to shared storage:

POST /v1/admin/storage/disable
X-Confirm-Tenant: <tenant UUID>
Content-Type: application/json

{
  "reason": "Reason description (minimum 10 characters)"
}

Effect:

  • The configuration is moved to disabled.
  • Every new upload goes back into KnowaKYC's shared storage.
  • An unfinished migration is cancelled (status cancelled).
  • Files already written to the tenant bucket are NOT deleted — the tenant retains full control over its bucket and can delete them on its own.
  • Those files remain available until the configuration is deleted.

The reason field is required (minimum 10 characters) — it is recorded in the audit log for incident analysis.

To connect another bucket later, you have to create a new configuration from scratch.

Deleting a configuration

DELETE /v1/admin/storage/configs/{id}

Only configurations in pending, validated, or failed status can be deleted. An active configuration must first be moved to disabled through the /disable endpoint.

Credentials encryption

All secrets (secret_key, service_account_json, Azure keys) are stored in the database in encrypted form:

  • Algorithm — AES-256-GCM (envelope encryption).
  • The DEK (Data Encryption Key) is derived from the KnowaKYC master key through HKDF-SHA256, with the tenant UUID as salt and a version-tagged info string.
  • The master key is stored in the STORAGE_MASTER_KEY environment variable (it does not overlap with ENCRYPTION_KEY).
  • When the encryption format changes, old ciphertexts can still be decrypted through credentials_version, while new ones are encrypted with the current version.

Secrets cannot be read out through the API — even KnowaKYC super-admins do not have an interface for extraction. If they are lost, the only recovery path is rotation (/rotate) with new credentials.

FAQ

How much does BYOB cost?

On the KnowaKYC side — free (the option is available on every plan). Storage and traffic costs are paid by the tenant directly to its cloud provider (AWS, GCP, Cloudflare, etc.).

Will upload latency increase?

Marginally. KnowaKYC uses presigned URLs — the applicant's browser uploads the file directly into the tenant bucket, bypassing the KnowaKYC API. Latency is determined by the chosen bucket region (pick the region closest to the majority of users).

What happens if AWS / KMS is unavailable?

Uploads during that period return a 5xx error; KnowaKYC does not "fall back" to shared automatically (that would break the compliance invariant). For long outages, the tenant administrator can run Disable — and writes go back into shared storage.

The health check records degradation and sends an email notification after several consecutive failures.

Can I have two active configurations at once?

No. A tenant can have only one active configuration at a time. To switch to another bucket, the current active configuration must first be Disabled, then a new one created.

Diagnostics

CheckTypical failure causeAction
head_bucketBucket does not exist / no s3:ListBucketVerify the bucket name; add the permission
put_probeNo s3:PutObjectCheck the permission policy
get_probe / delete_probeNo s3:GetObject / s3:DeleteObjectAdd the permissions
presign_getEndpoint is misconfiguredVerify endpoint for S3-compat
public_accessThe bucket responds to anonymous requestsEnable Block Public Access (AWS) / publicAccessPrevention=enforced (GCS) / allowBlobPublicAccess=false (Azure)
clock_skewClock skew exceeds 60 sConfigure NTP on the source server
multipart_uploadProvider does not support multipart, or no permissions for partsVerify multipart support with the provider
copy_objectNo s3:CopyObject, or cross-bucket copy is blockedAdd s3:GetObject + s3:PutObject on source and target buckets

All errors are also recorded in the audit log (storage_config_validated / storage_config_activated / storage_config_disabled).

Troubleshooting

Progress shows 0 / 0 with non-zero error count

If you still see 0 / 0 on a fresh activation, the underlying file inventory is empty — there are no historical files in shared storage to migrate, only new uploads will appear. This is the expected state for a new tenant.

Status Completed but the row reports failed objects

If some files fail to copy, the job moves to failed. KnowaKYC retries it automatically, and the panel exposes a Retry migration button for manual immediate retry.

If the job stays failed after several attempts, inspect the last_error text on the panel. The most common causes:

  • signature does not match — credentials contain whitespace, or region does not match the endpoint URL segment. Rotate credentials, paste them WITHOUT trailing newlines.
  • permission denied — bucket policy denies s3:PutObject or the IAM role lacks an inline policy. Re-check the policy from the wizard.
  • bucket not found — the bucket was deleted or renamed out of band.

Health code reference

The health panel shows a short error code and a localised hint. The full mapping:

CodeMeaningAction
CONFIG_NOT_FOUNDStorage config row missing in DBRe-run Activate
CREDENTIALS_INVALIDDriver rejected the credentialsRotate with fresh keys
EXTERNAL_ID_MISMATCHAWS AssumeRole external ID differsRe-paste the External ID into the IAM trust policy
TENANT_STORAGE_UNAVAILABLEProvider transiently unreachableWait for next probe; if persistent — check with provider
PERMISSION_DENIEDBucket policy blocks the operationAdjust the bucket / IAM policy
BUCKET_NOT_FOUNDBucket deleted out of bandRe-create the bucket OR Disable + new config
SIGNATURE_MISMATCHRegion drift, or whitespace in keysRotate credentials, fix the region field
HEALTH_CHECK_FAILEDGeneric catch-allSee last_error for details

Path-style addressing

S3-compatible providers differ in their URL style. Some buckets use path-style URLs (host/bucket/key); others use virtual-hosted URLs (bucket.host/key).

KnowaKYC stores the selected value and uses it as-is when creating the storage provider. The next Validate step probes the bucket with that exact mode; if the provider rejects the addressing style, go back and switch Use path-style, then validate again.

See also: Audit log, Tenant settings.

On this page