Create a gRPC security policy with external files
This guide explains how to protect gRPC services by referencing external .proto files from an F5 WAF for NGINX policy in F5 NGINX Instance Manager.
Using the F5 NGINX Instance Manager REST API, you can attach external files directly to a WAF policy request — no need to place files on the Instance Manager host before compilation. You can upload .proto IDL files for gRPC protection, OpenAPI specification files (.yaml, .yml, .json) for API protection, and .tgz bundles that package .proto, .yaml, .yml, .json files with their directory structure. When you create or update a policy through the API, Instance Manager does three things:
- Accepts the files with the policy, in the same API request.
- Validates the references in both directions. Every
file://reference in the policy must have a matching uploaded file, and every uploaded file must be referenced by the policy. - Cleans up automatically. Instance Manager writes the files to a temporary directory for compilation, then removes the directory when compilation finishes.
After compilation, the .proto files are bundled with the compiled policy and delivered to your NGINX instances.
This feature is available in F5 NGINX Instance Manager 2.24.0 and later, and only through the REST API. The web interface doesn’t support it in this release.
Make sure you have:
- NGINX Instance Manager 2.24.0 with F5 WAF for NGINX policy management set up. See How WAF policy management works.
- An access token or basic authentication credentials for the REST API. The examples in this topic use an access token. To use basic authentication instead, replace the
Authorizationheader with-u <USERNAME>:<PASSWORD>. - The
.protofile for your gRPC service, plus every file it imports, directly or indirectly.
In a policy, a gRPC profile lists its .proto files in idlFiles. Each entry
points to an uploaded file using a file:// URI:
- Identify the primary
.protofile. This is the file that defines the gRPC service. - Collect every file that the primary file imports, and every file those files import. NGINX Instance Manager doesn’t discover imports for you, so list each file explicitly, and upload each file.
For more about how F5 WAF for NGINX uses .proto files in gRPC content profiles,
including supported proto3 syntax requirements and import resolution, see
gRPC protection.
-
Check that your files meet these limits:
Limit JSON request Multipart request Maximum number of files 50 100 Maximum size per file 2 MB 2 MB Maximum total size 5 MB (decoded) 5 MB compressed, 10 MB uncompressed The base64-encoded policy
contentfield can be up to 3 MB. -
Check the file types. NGINX Instance Manager accepts the following types, and you set the type of each file in the
typefield of itsfile_referencesentry:typevalueFile extensions Use for proto.protogRPC service definition files openapi.yaml,.yml,.jsonOpenAPI specification files for API protection tgz.tgz,.tarIDL bundle: .protofiles packaged with their directory structure (experimental). Note:.tar.gzis not a supported extension — use.tgzor.tar.If an archive contains any other file type, NGINX Instance Manager rejects the whole archive.
-
Check that your
.protofiles meet the WAF compiler requirements:- They use Protocol Buffers version 3 syntax (
syntax = "proto3";). - The primary file defines at least one
serviceblock with at least onerpcmethod. - Every imported file is also uploaded, either as a separate
file_referencesentry or in the same bundle, withimportUrlset on each secondary file to match the import prefix.
The WAF compiler validates syntax at compile time. If a file has errors, compilation returns a message that describes the failure.
- They use Protocol Buffers version 3 syntax (
To upload many .proto files in one entry, package them with their directory structure in a .tgz bundle. The following steps use these sample files. Save each file to the path shown:
Sample files
payment_service.proto is the primary gRPC service definition:
syntax = "proto3";
package payments;
service PaymentService {
rpc ProcessPayment(PaymentRequest) returns (PaymentResponse);
}
message PaymentRequest {
string customer_id = 1;
string description = 2;
}
message PaymentResponse {
string transaction_id = 1;
string status = 2;
}common/money.proto is a supporting type that the service imports:
syntax = "proto3";
package common;
message Money {
string currency_code = 1;
int64 amount_micros = 2;
}payment_api.yaml is an OpenAPI specification for API protection policies:
openapi: "3.0.0"
info:
title: Payment API
version: "1.0"
paths:
/payments/{id}:
get:
summary: Get payment by ID
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
"200":
description: Payment details-
Create a directory structure that matches the import paths:
shell mkdir -p grpc_bundle/grpc_files/common cp payment_service.proto grpc_bundle/grpc_files/ cp common/money.proto grpc_bundle/grpc_files/common/ -
Create the bundle:
shell cd grpc_bundle tar -czf payment_bundle.tgz grpc_files/
Add a grpc-profiles section to your policy JSON. For each .proto file, add an entry to idlFiles.
| Field | Required | Description |
|---|---|---|
hasIdlFiles |
Yes | Set to true when the profile has idlFiles. |
associateUrls |
No | Set to true to associate URLs with the profile from google.api.http annotations in your .proto files. If you set this field, every .proto file must have those annotations. Otherwise, apcompile fails. The example in this topic omits this field. |
idlFiles[].idlFile.$ref |
Yes | The file:/// URI of the file. It must match an uploaded file path. |
idlFiles[].isPrimary |
Yes, for one file when you reference individual .proto files |
Set to true for exactly one file in each profile. Don’t use it with a .tgz bundle. |
idlFiles[].primaryIdlFileName |
Yes, for a .tgz bundle |
The entry-point .proto file inside the bundle, as a relative path as it appears in the bundle, for example grpc_files/payment_service.proto. |
idlFiles[].importUrl |
Required for non-primary files that other files import. Not needed with a .tgz bundle |
The directory portion of the path used in import statements. See Set importUrl. |
The following example defines a profile with one primary file and four imported files:
{
"policy": {
"name": "acme-grpc-policy",
"enforcementMode": "blocking",
"template": { "name": "POLICY_TEMPLATE_NGINX_BASE" },
"applicationLanguage": "utf-8",
"grpc-profiles": [
{
"name": "payment-service-profile",
"attackSignaturesCheck": true,
"hasIdlFiles": true,
"idlFiles": [
{
"idlFile": { "$ref": "file:///grpc_files/payment_service.proto" },
"isPrimary": true
},
{
"idlFile": { "$ref": "file:///grpc_files/common/money.proto" },
"importUrl": "common"
}
]
}
]
}
}When a .proto file contains an import statement, the WAF compiler uses importUrl to find the imported file. Set importUrl on every non-primary file to the directory portion of the path used in the import statement. If you omit it, compilation can fail with a File not found error even when the file is uploaded.
| Path of the file | Import statement | importUrl |
|---|---|---|
/payments/billing/invoice.proto |
import "payments/billing/invoice.proto" |
payments/billing |
/common/pagination.proto |
import "common/pagination.proto" |
common |
/service.proto (top level) |
import "service.proto" |
Empty, or omit the field |
The primary file doesn’t need importUrl.
When you upload a .tgz bundle, reference the bundle in a single idlFiles entry and use primaryIdlFileName to identify the entry-point file inside it. Don’t set isPrimary or importUrl:
{
"policy": {
"name": "acme-grpc-policy",
"template": { "name": "POLICY_TEMPLATE_NGINX_BASE" },
"applicationLanguage": "utf-8",
"enforcementMode": "blocking",
"grpc-profiles": [
{
"name": "acme-service-profile",
"hasIdlFiles": true,
"idlFiles": [
{
"idlFile": { "$ref": "file:///grpc_files/payment_bundle.tgz" },
"primaryIdlFileName": "grpc_files/payment_service.proto"
}
]
}
]
}
}In a JSON request, you must Base64-encode the policy JSON and every file. Paste the output as the value of the content field.
# Linux
base64 -w 0 <FILE_NAME>
# macOS
base64 <FILE_NAME>Replace <FILE_NAME> with the policy JSON, a .proto file, an OpenAPI file, or a .tgz bundle.
| Scenario | Upload path |
|---|---|
Individual .proto files (any number, up to the limits) |
JSON |
One .tgz bundle with a decoded size of 2 MB or less |
JSON, with the Base64-encoded bundle in content |
One .tgz bundle with a decoded size over 2 MB |
Multipart, with the bundle inside an outer .tar.gz archive |
Multiple .tgz bundles, up to 10 MB uncompressed in total |
Multipart, with all bundles inside one outer .tar.gz archive |
Choose one of the following methods. Use multipart for large proto trees.
Send the files in the file_references array of a JSON request. Base64-encode each file.
curl -X POST https://<NIM_FQDN>/api/platform/v1/security/policies \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-H "Content-Type: application/json" \
-d @grpc-policy-request.jsonReplace <NIM_FQDN> with the fully qualified domain name (FQDN) of your NGINX Instance Manager host and <ACCESS_TOKEN> with your access token.
JSON request
{
"metadata": {
"name": "acme-grpc-policy",
"displayName": "Acme gRPC policy"
},
"content": "<BASE64_ENCODED_POLICY>",
"file_references": [
{
"file_path": "/acme_service.proto",
"type": "proto",
"content": "<BASE64_ENCODED_FILE>"
},
{
"file_path": "/payments/amount.proto",
"type": "proto",
"content": "<BASE64_ENCODED_FILE>"
}
]
}Each file_references entry has the following fields:
| Field | Required | Description |
|---|---|---|
file_path |
Yes | The absolute path of the file (starts with /). It must match the $ref path in the policy without the file:// prefix. |
type |
Yes | The file type. Set to proto, openapi, or tgz. Instance Manager doesn’t infer the type from the file extension, so you must set it for every file. |
content |
Yes | The Base64-encoded file content. |
To upload a .tgz bundle, add one entry with "type": "tgz" and the Base64-encoded bundle in content. The decoded bundle must be 2 MB or less:
{
"metadata": {
"name": "acme-grpc-policy",
"displayName": "Acme gRPC policy - bundle"
},
"content": "<BASE64_ENCODED_POLICY>",
"file_references": [
{
"type": "tgz",
"file_path": "/grpc_files/payment_bundle.tgz",
"content": "<BASE64_ENCODED_BUNDLE>"
}
]
}To upload an OpenAPI specification, add an entry with "type": "openapi":
{
"metadata": {
"name": "acme-api-policy",
"displayName": "Acme API protection policy"
},
"content": "<BASE64_ENCODED_POLICY>",
"file_references": [
{
"type": "openapi",
"file_path": "/openapi_files/payment_api.yaml",
"content": "<BASE64_ENCODED_FILE>"
}
]
}The policy JSON for this request references the file in open-api-files:
{
"policy": {
"name": "acme-api-policy",
"template": { "name": "POLICY_TEMPLATE_NGINX_BASE" },
"applicationLanguage": "utf-8",
"enforcementMode": "blocking",
"open-api-files": [
{ "link": "file:///openapi_files/payment_api.yaml" }
]
}
}Send the files as a single .tar.gz archive in the file_references_archive field of a multipart/form-data request.
-
Create the archive from inside the directory that contains your proto root. The path of each entry in the archive must match the path in the policy
$ref, without thefile://prefix.shell cd /path/to/proto/root tar czf /tmp/archive.tar.gz acme_service.proto common/Don’t create the archive from a parent directory. An extra directory prefix, such as
./myservice/payments/amount.proto, doesn’t match the policy pathfile:///payments/amount.proto, and compilation fails. -
Check the entry paths:
tar -tvf /tmp/archive.tar.gzThe entries should look like
./payments/amount.proto, with no extra leading directory. -
Send the request:
shell curl -X POST https://<NIM_FQDN>/api/platform/v1/security/policies \ -H "Authorization: Bearer <ACCESS_TOKEN>" \ -F 'metadata={"name":"acme-grpc-policy","displayName":"Acme gRPC policy"};type=application/json' \ -F "content=$(base64 -w 0 policy.json)" \ -F "file_references_archive=@/tmp/archive.tar.gz;type=application/gzip"The request has the following fields:
Field Required Description metadataYes A JSON-encoded string with the policy name. You can also includedisplayNameanddescription.contentYes The Base64-encoded policy JSON, as a string. file_references_archiveYes The .tar.gzarchive of your.protofiles, uploaded as a binary file.
Archive entries must use relative paths. Instance Manager rejects archives that contain .. or absolute paths.
Multipart upload support for .tgz IDL bundles — where one or more .tgz files are packaged inside an outer .tar.gz transport archive — is experimental in this release. The behavior may change in a future release. For a single .tgz bundle whose decoded size is 2 MB or less, use the JSON method instead.
Use this path when you have:
- More than one
.tgzbundle, or - A single
.tgzbundle with a decoded size over 2 MB
Put all the .tgz files inside one outer .tar.gz transport archive:
-
Copy each
.tgzbundle to the path that the policy$refuses. For the bundles in Reference a .tgz bundle, the$refvalues arefile:///grpc_files/payment_bundle.tgzandfile:///grpc_files/auth_bundle.tgz:shell mkdir -p transport/grpc_files cp grpc_bundle/payment_bundle.tgz transport/grpc_files/ cp grpc_bundle/auth_bundle.tgz transport/grpc_files/ -
Create the outer archive from inside the
transportdirectory, so the entry paths have no extra leading directory:shell cd transport tar czf /tmp/archive.tar.gz grpc_files/ -
Check that the entries match the
$refpaths:tar -tvf /tmp/archive.tar.gzThe output should list:
text grpc_files/payment_bundle.tgz grpc_files/auth_bundle.tgz -
In the policy JSON, reference each bundle with its own
idlFilesentry and its ownprimaryIdlFileName:json "idlFiles": [ { "idlFile": { "$ref": "file:///grpc_files/payment_bundle.tgz" }, "primaryIdlFileName": "payment_service.proto" }, { "idlFile": { "$ref": "file:///grpc_files/auth_bundle.tgz" }, "primaryIdlFileName": "auth_service.proto" } ] -
Send the request with the outer archive in the
file_references_archivefield:shell curl -X POST https://<NIM_FQDN>/api/platform/v1/security/policies \ -H "Authorization: Bearer <ACCESS_TOKEN>" \ -F 'metadata={"name":"acme-grpc-policy","displayName":"Acme gRPC policy"};type=application/json' \ -F "content=$(base64 -w 0 policy.json)" \ -F "file_references_archive=@/tmp/archive.tar.gz;type=application/gzip"Instance Manager extracts the
.tgzbundles from the outer archive, stores each one, and supplies them to the compiler at compile time.
ExperimentalPackaging.tgzbundles inside an outer.tar.gzfor multipart upload is experimental in this release and subject to change in a future release. Do not use this method in production environments without testing. For stable single-bundle workflows, use the JSON body method described in Send the request.
If the request succeeds, Instance Manager returns 201 Created and the policy metadata, including the policy UID. By default, the response doesn’t list the stored file references. To include the file reference metadata (path, type, size, and hash), add the includePolicyContent=true query parameter to the request.
ThePUTmethod is deprecated for updating a policy. To change the files or the policy, send aPOSTrequest with a JSON body that includesfile_referencesto/api/platform/v1/security/policies?isNewRevision=true. This creates a new revision of the policy. You can’t change the policy name when you create a new revision. Multipart requests aren’t supported when you update a policy. See Update a security policy.
A policy can’t use both
openAPISpecContent(legacy) andfile_referencesin the same request. To upload OpenAPI specification files with your.protofiles, usefile_references:
- Add an entry with
"type": "openapi". Thefile_pathmust end in.yaml,.yml, or.json.- Reference the file in the
open-api-filessection of the policy with afile:///URI, for example"link": "file:///openapi_files/spec.yaml".
- Check that the policy compiled and that its bundle includes your
.protofiles.
To check compilation status in the web interface:
- Go to Security > Policies.
- Select the policy you created.
- Select the … actions menu, then select Compile.
- The compilation status appears on the policy detail page. If compilation fails, the error message describes the specific failure.
To retrieve a compiled bundle using the API, send a GET request to the policy endpoint with includePolicyContent=true:
-
Reference the compiled policy in your NGINX configuration and publish it to your instances.
- Add the
app_protect_policy_filedirective to the server that handles gRPC traffic. See Add WAF configuration and Edit WAF configuration. - Publish the policy to an NGINX instance or instance group. See Publish updates to instances.
- Confirm that the deployment succeeded. See Check publication status.
- Add the
-
Send test requests to the gRPC service and confirm that the policy blocks attacks.
To stop protecting the gRPC service:
- Remove the
app_protect_policy_filedirective that references the policy, or setapp_protect_enabletooff, in the NGINX configuration for the gRPC server. See Edit WAF configuration. - Publish the updated configuration to your instances. See Publish updates to instances.
- Optional: Delete the policy if you no longer need it. See Delete a security policy.
- Send the same test requests that you used to verify the policy. The service should no longer block them.
When you delete a policy:
- Stored file references are deleted. Instance Manager removes all
.protofiles and other file references associated with the policy from its database. This operation can’t be undone. - Temporary compilation files are already removed. Instance Manager writes files to a temporary directory only for the duration of each compilation, and removes that directory when compilation finishes. There are no temporary files to clean up when you delete a policy.
- Published NGINX configurations are not affected immediately. If the policy is still referenced by a published NGINX configuration, NGINX continues to enforce the last compiled policy bundle until you publish an updated configuration. Deleting the policy from Instance Manager does not automatically update NGINX instances. Removing the
app_protect_policy_filedirective (step 1 above) and publishing the updated configuration is what stops enforcement.
Instance Manager validates file references before it stores the policy.
The error response has code 13102 when creating a policy, or 13103 when updating
a policy with isNewRevision=true. The reason is in details[0].description.
| Error | Cause | Fix |
|---|---|---|
policy contains a file reference but no files were uploaded |
The policy has a file:// reference, but the request has no files. |
Upload the files, or remove the reference. |
remote URL references are not supported |
A $ref uses an http:// or https:// URL. |
Upload the file and use a file:/// URI. |
referenced file not found in uploaded file references |
A $ref path doesn’t match an uploaded path. Matching is case-sensitive. |
Make the file_path match the $ref path exactly. |
file path must be absolute |
A file_path doesn’t start with /. |
Use /grpc_files/service.proto, not grpc_files/service.proto. |
uploaded file is not referenced by the policy |
A file:// URI in the policy JSON that has no corresponding entry in file_references (missing upload). |
A file in file_references has no matching file:// reference in the policy. |
archive contains a file with an unsupported extension |
The archive has a file that isn’t .proto, .yaml, .yml, .json, .tgz, or .tar. |
Remove the file from the archive. |
the uploaded file is not a valid .tar.gz archive |
The archive isn’t gzip-compressed tar. | Recreate the archive with tar czf. |
an archive entry contains an unsafe path |
An entry uses .. or an absolute path. |
Recreate the archive with relative paths. |
uploaded file is not referenced by any file:// URI in the policy |
A file in file_references was uploaded but the policy has no matching file:// URI for it. Every uploaded file must be referenced. |
Add a $ref in the policy for the file, or remove it from file_references. |
malformed file:// URI: "file://host/path" format is not supported |
A $ref uses two slashes followed by a path separator, for example file://etc/myfile.proto. The segment before the / is interpreted as a hostname. |
Use three slashes: file:///absolute/path. |
If compilation fails with File not found, check these causes:
- Archive paths don’t match the policy. Compare the output of
tar -tvf archive.tar.gzwith the$refpaths in the policy. - A non-primary file has no
importUrl. AddimportUrlfor every file that other files import. - A transitive import is missing. Add every file that the primary file imports, directly or indirectly, to both the upload and
idlFiles.
Instance Manager doesn’t validate .proto syntax. The WAF compiler does, so a syntax error appears as a compilation error, not as an upload error. The compilation error describes the specific failure.