Skip to content

Security Guide

Guide to securing your Pulsing cluster with TLS encryption.

Overview

Pulsing supports passphrase-based mTLS (Mutual TLS) for secure cluster communication. This innovative design provides:

  • Zero-configuration PKI: No need to generate or distribute certificates manually
  • Passphrase-based access: Nodes with the same passphrase can join the cluster
  • Cluster isolation: Different passphrases create completely isolated clusters
  • Mutual authentication: Both server and client verify each other's certificates

Pickle serialization risk (RCE)

Pulsing currently uses Python Pickle for Python-to-Python message payloads. Never accept untrusted payloads or expose Pulsing ports to an untrusted network.

Production guidance:

  • Enable mTLS by setting a passphrase (required for any real deployment)
  • Network isolation (private VPC/subnet + firewall) is still recommended
  • Treat the cluster as a trusted boundary until non-pickle codecs are the default

Enabling TLS

Development Mode (No TLS)

By default, Pulsing uses cleartext HTTP/2 (h2c) for easy debugging:

import pulsing as pul

# No passphrase - uses cleartext HTTP/2
system = await pul.actor_system(addr="0.0.0.0:8000")

Production Mode (mTLS)

To enable TLS encryption, simply set a passphrase:

# Set passphrase - automatically enables mTLS
system = await pul.actor_system(
    addr="0.0.0.0:8000",
    passphrase="my-cluster-secret"
)

Multi-Node Cluster with TLS

All nodes in a cluster must use the same passphrase to communicate:

# Node 1: Seed node with TLS
system1 = await pul.actor_system(
    addr="0.0.0.0:8000",
    passphrase="shared-secret"
)

# Node 2: Join cluster with same passphrase
system2 = await pul.actor_system(
    addr="0.0.0.0:8001",
    seeds=["192.168.1.1:8000"],
    passphrase="shared-secret"  # Must match!
)

Passphrase Mismatch

Nodes with different passphrases cannot communicate. The TLS handshake will fail.

Cluster Isolation

Different passphrases create completely isolated clusters:

# Cluster A
system_a = await pul.actor_system(addr="0.0.0.0:8000", passphrase="secret-a")

# Cluster B (different passphrase)
system_b = await pul.actor_system(addr="0.0.0.0:9000", passphrase="secret-b")

# system_a and system_b cannot communicate

How It Works

Pulsing uses a deterministic CA derivation approach:

┌─────────────────────────────────────────────────────────────┐
│                    Passphrase (口令)                        │
│                         "my-secret"                         │
└──────────────────────────┬──────────────────────────────────┘
                           │ HKDF-SHA256
┌─────────────────────────────────────────────────────────────┐
│              Deterministic CA Certificate                   │
│         (Same passphrase → Same CA cert/key)                │
│         Algorithm: Ed25519 | Validity: 10 years             │
└──────────────────────────┬──────────────────────────────────┘
                           │ Signs
┌─────────────────────────────────────────────────────────────┐
│              Node Certificate (per node)                    │
│         (Each node generates its own, signed by CA)         │
│         CN: "Pulsing Node <uuid>" | SAN: pulsing.internal   │
└─────────────────────────────────────────────────────────────┘

Key Features

Feature Description
Mutual Authentication Both server and client present certificates
Passphrase = Access Only nodes knowing the passphrase can join
Zero-config PKI No manual certificate generation/distribution
Deterministic CA Same passphrase → Same CA (all nodes trust it)
Isolated Clusters Different passphrases = completely separate

Security Best Practices

1. Use Strong Passphrases

Passphrase Strength

Use high-entropy random strings for production:

# Good: High entropy
passphrase = "aX9#mK2$nL5@pQ8&"

# Bad: Weak/predictable
passphrase = "password123"

2. Environment Variables

Store passphrases in environment variables, not code:

import os
import pulsing as pul

passphrase = os.environ.get("PULSING_PASSPHRASE")

# Create system with optional TLS
system = await pul.actor_system(
    addr="0.0.0.0:8000",
    passphrase=passphrase  # None = no TLS (dev mode)
)

3. Rotate Passphrases

To rotate passphrases:

  1. Deploy new nodes with the new passphrase
  2. Gradually migrate actors to new nodes
  3. Decommission old nodes

Rolling Updates

Nodes with different passphrases cannot communicate. Plan for a brief transition period.

4. Network Segmentation

Even with TLS, use network-level security:

  • Deploy in private VPCs/subnets
  • Use firewalls to restrict access
  • Consider VPN for cross-datacenter communication

Comparison with Other Frameworks

Aspect Pulsing Ray Traditional mTLS
Configuration 1 line of code Multiple config files PKI infrastructure required
Certificate Management None needed Need to distribute certs Need CA + cert rotation
New Node Join Know passphrase Pre-configure certificates Issue new certificates
Cluster Isolation Different passphrase Different cert system Different CA
Crypto Algorithm Ed25519 + mTLS TLS 1.2/1.3 Depends on config

Limitations

Current Limitations

  • No authorization: Any actor can call any actor (authentication only, not authorization)
  • Pickle serialization: Message payloads still use Pickle (plan to replace with msgpack)
  • No cert rotation: Changing passphrase requires cluster restart

Example: Secure Distributed Counter

import os
import pulsing as pul

# Get passphrase from environment
PASSPHRASE = os.environ.get("PULSING_SECRET", None)

@pul.remote
class SecureCounter:
    def __init__(self, init_value: int = 0):
        self.value = init_value

    def get(self) -> int:
        return self.value

    def increment(self, n: int = 1) -> int:
        self.value += n
        return self.value

async def main():
    # Create system with optional TLS
    system = await pul.actor_system(
        addr="0.0.0.0:8000",
        passphrase=PASSPHRASE
    )

    # Spawn secure counter
    counter = await SecureCounter.local(system, init_value=0)

    print("Secure counter running...")
    print(f"TLS enabled: {PASSPHRASE is not None}")

    await system.shutdown()

Next Steps