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:
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:
- Deploy new nodes with the new passphrase
- Gradually migrate actors to new nodes
- 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¶
- Learn about Remote Actors for cluster communication
- Check HTTP2 Transport for transport details
- Read Semantics for message delivery guarantees