Skip to main content
Template signing provides cryptographic verification for Nuclei templates, especially those using the code protocol. Digital signatures ensure template authenticity and prevent unauthorized modifications.

Why sign templates?

Template signing addresses critical security concerns:

Code execution safety

Code protocol templates execute arbitrary code. Signatures verify trusted authorship.

Tamper detection

Detect any modifications to templates after signing

Author verification

Confirm template origin and chain of trust

Community trust

Build confidence in shared templates through cryptographic proof

When signatures are required

Templates using the code protocol must be signed before execution. This includes templates with code: or file imports in sensitive fields.
Examples requiring signatures:
# Code protocol - requires signing
id: bash-command-check

info:
  name: Bash Command Check
  author: pdteam
  severity: info

code:
  - engine: bash
    source: |
      #!/bin/bash
      echo "Checking system"
# JavaScript code - requires signing
id: cloud-check

info:
  name: Cloud Resource Check
  author: pdteam
  severity: info

javascript:
  - code: |
      const aws = require('aws-sdk');
      // Cloud check logic

Signature mechanism

Nuclei uses ECDSA (Elliptic Curve Digital Signature Algorithm) with SHA-256 for template signing.

Signature format

Signatures are appended to templates as comments:
id: example-template

info:
  name: Example Template
  author: pdteam
  severity: info

code:
  - engine: bash
    source: echo "test"

# digest: 4a5e6c7d8e9f0a1b2c3d4e5f6a7b8c9d:a1b2c3d4e5f6
Signature components (from pkg/templates/signer/tmpl_signer.go:24-25):
# digest: <hex-encoded-signature>:<user-fragment>
  • Signature: ECDSA signature encoded as hex
  • User fragment: MD5 hash of public key for re-signing verification

Setting up signing

Generate signing keys

On first use, Nuclei generates a key pair automatically:
nuclei -sign -t template.yaml
1

Key generation prompt

Enter your user or organization name:
[*] Enter User/Organization Name (exit to abort): ProjectDiscovery
2

Set passphrase

Optionally protect the private key with a passphrase:
[*] Enter passphrase (exit to abort): ********
[*] Enter same passphrase again: ********
Press Enter without input to skip passphrase protection.
3

Keys saved

Keys are saved to the configuration directory:
~/.config/nuclei/keys/nuclei-user.crt
~/.config/nuclei/keys/nuclei-user-private-key.pem

Key generation internals

From pkg/templates/signer/handler.go:132-170:
func (k *KeyHandler) GenerateKeyPair() {
    // Generate ECDSA P-256 private key
    privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
    
    // Create self-signed certificate
    cert := x509.Certificate{
        Subject: pkix.Name{
            CommonName: identifier,  // User/Org name
        },
        SignatureAlgorithm: x509.ECDSAWithSHA256,
        NotAfter: notBefore.Add(4 * 365 * 24 * time.Hour),  // 4 years
    }
}
Certificates are self-signed and valid for 4 years. They use the P-256 elliptic curve.

Signing templates

Sign a template

Use the -sign flag to sign templates:
nuclei -sign -t template.yaml
Sign a specific template:
nuclei -sign -t code-template.yaml

Signing process

The signing process (from pkg/templates/template_sign.go:59-88):
1

Load template

Parse the template and resolve file imports
2

Extract existing signature

Remove any existing signature from template content
3

Include file imports

Append contents of imported files to signing data:
for _, file := range tmpl.GetFileImports() {
    bin, _ := os.ReadFile(file)
    buff.Write(bin)
}
4

Generate signature

Create ECDSA signature using SHA-256 hash:
dataHash := sha256.Sum256(data)
ecdsaSignature, err := ecdsa.SignASN1(rand.Reader, privateKey, dataHash[:])
5

Append signature

Add signature as a comment to the template file

File imports in signatures

When templates reference external files, their contents are included in the signature:
code:
  - engine: bash
    source: script.sh  # File content included in signature
The signing process loads referenced files and includes them in the signature calculation (see pkg/templates/signer/tmpl_signer.go:98-106).

Verifying signatures

Automatic verification

Nuclei automatically verifies template signatures during execution:
nuclei -t signed-template.yaml -u https://example.com
For code protocol templates:
# Requires code protocol enabled
nuclei -t signed-code-template.yaml -u https://example.com -code

Verification process

From pkg/templates/signer/tmpl_signer.go:131-165:
1

Extract signature

Parse the signature comment from template:
signature, content := ExtractSignatureAndContent(data)
2

Decode signature

Convert hex-encoded signature to bytes
3

Normalize content

Remove \r\n line endings for cross-platform compatibility:
content = bytes.ReplaceAll(content, []byte("\r\n"), []byte("\n"))
4

Include file imports

Load and append imported file contents (same as signing)
5

Verify ECDSA signature

Validate signature against public key:
dataHash := sha256.Sum256(data)
verified := ecdsa.VerifyASN1(publicKey, dataHash[:], signature)

Verification failures

Templates with invalid signatures cannot be executed when code protocol is enabled.
Common verification failures:
Cause: Code protocol template lacks signature.Solution: Sign the template:
nuclei -sign -t template.yaml
Cause: Signature not in expected format or location.Solution: Ensure signature is the last line and starts with # digest:.
Cause: Content modified after signing.Solution: Re-sign the template after making changes.
Cause: Incorrect passphrase for encrypted private key.Solution: Enter the correct passphrase when prompted.

Re-signing templates

Re-signing restrictions

Code protocol templates can only be re-signed by the original signer. This prevents malicious modifications.
From pkg/templates/signer/tmpl_signer.go:77-95:
if tmpl.HasCodeProtocol() {
    if len(existingSignature) > 0 {
        // Extract user fragment from existing signature
        fragment := arr[2]
        
        // Verify current signer matches original signer
        if fragment != t.GetUserFragment() {
            return "", errkit.New("re-signing code templates are not allowed for security reasons.")
        }
    }
}

User fragment

The user fragment identifies the signer using an MD5 hash of their public key:
// From pkg/templates/signer/tmpl_signer.go:62-70
func (t *TemplateSigner) GetUserFragment() string {
    if t.handler.ecdsaPubKey != nil {
        hashed := md5.Sum(t.handler.ecdsaPubKey.X.Bytes())
        return fmt.Sprintf("%x", hashed)
    }
}

Custom certificate management

Environment variables

Override default certificate locations:
# Point to custom certificate
export NUCLEI_USER_CERTIFICATE=/path/to/custom.crt

# Point to custom private key
export NUCLEI_USER_PRIVATE_KEY=/path/to/custom-key.pem

# Or provide file paths directly
export NUCLEI_USER_CERTIFICATE=/home/user/certs/nuclei.crt
export NUCLEI_USER_PRIVATE_KEY=/home/user/certs/nuclei-key.pem

Programmatic signing

Use the signing API programmatically:
import (
    "github.com/projectdiscovery/nuclei/v3/pkg/templates"
    "github.com/projectdiscovery/nuclei/v3/pkg/templates/signer"
)

// Create signer from files
templateSigner, err := signer.NewTemplateSignerFromFiles(
    "cert.crt",
    "private-key.pem",
)

// Sign template
err = templates.SignTemplate(templateSigner, "template.yaml")

Certificate details

From pkg/templates/signer/handler.go:23-30:
const (
    CertType           = "PD NUCLEI USER CERTIFICATE"
    PrivateKeyType     = "PD NUCLEI USER PRIVATE KEY"
    CertFilename       = "nuclei-user.crt"
    PrivateKeyFilename = "nuclei-user-private-key.pem"
)
Certificate structure:
  • Type: X.509 certificate
  • Algorithm: ECDSA with SHA-256
  • Curve: P-256 (secp256r1)
  • Validity: 4 years from generation
  • Subject: Common Name = User/Organization identifier

Security considerations

Private key protection

Never share or commit private keys. They should remain secure on your local system.
1

Use passphrase protection

Encrypt private keys with a strong passphrase:
# Passphrase prompted during key generation
[*] Enter passphrase: ********
2

Restrict file permissions

Ensure keys have appropriate permissions:
chmod 600 ~/.config/nuclei/keys/nuclei-user-private-key.pem
chmod 644 ~/.config/nuclei/keys/nuclei-user.crt
3

Backup keys securely

Store backup copies in a secure location (not in version control)

Template verification in workflows

Always verify signatures in production:
.github/workflows/scan.yml
- name: Run Nuclei Scan
  run: |
    # Signatures verified automatically
    nuclei -t signed-templates/ -u ${{ secrets.TARGET_URL }} -code

Testing signature functionality

Example test cases from pkg/templates/signer/tmpl_signer_test.go:36-90:
// Sign template
signature, err := signer.Sign(data, tmpl)
signedData := append(data, []byte("\n"+signature)...)

// Verify signature
verified, err := signer.Verify(signedData, tmpl)
// verified == true

Troubleshooting

Issue: nuclei user certificate not foundSolution: Generate keys first:
nuclei -sign -t template.yaml
# Follow prompts to generate keys
Issue: Private key is encrypted.Solution: Either enter passphrase each time or regenerate without passphrase.
Issue: given filePath is not a templateSolution: Workflow templates (templates with workflows: section) cannot be signed. Only single protocol templates support signing.
Issue: re-signing code templates are not allowed for security reasonsSolution: You’re trying to re-sign a code template originally signed by someone else. Only the original signer can re-sign code templates.

Advanced usage

Skip key generation

For CI/CD environments where keys should be pre-configured:
import "github.com/projectdiscovery/nuclei/v3/pkg/templates/signer"

// Skip automatic key generation
signer.SkipGeneratingKeys = true

Allow local file access

For signing templates that reference local files:
import "github.com/projectdiscovery/nuclei/v3/pkg/templates"

// Enable local file access for signing
templates.TemplateSignerLFA()

Best practices

✓ Always sign code protocol templates before distribution✓ Protect private keys with passphrases✓ Store keys securely, never in version control✓ Verify signatures in production environments✓ Use environment variables for custom certificate paths✓ Test signed templates thoroughly before sharing✓ Document signing requirements for contributors✓ Backup keys in secure, encrypted storage

Next steps

Contributing templates

Learn how to contribute signed templates

Code protocol

Understand code protocol templates

Best practices

Follow template development guidelines

Validation

Validate templates before signing

Build docs developers (and LLMs) love