NOTE: This provider is auto-generated.
This software is a pre-release version and is not ready for production use.
- No Warranty: This software is provided "as is," without any warranty of any kind, either expressed or implied, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose.
- Instability: The beta software may contain defects, may not operate correctly, and may be substantially modified or withdrawn at any time.
- Limitation of Liability: In no event shall the authors or copyright holders be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the beta software or the use or other dealings in the beta software.
- Feedback: We encourage and appreciate your feedback and bug reports. However, you acknowledge that any feedback you provide is non-confidential.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THIS SOFTWARE IS RELEASED AS A PROOF OF CONCEPT FOR EXPERIMENTAL PURPOSES ONLY. USE IT AT OWN RISK. THIS SOFTWARE IS NOT SUPPORTED.
-
Install Go
-
Clone the SDK repo:
git clone https://github.com/paloaltonetworks/scm-go- Clone this repo:
git clone https://github.com/paloaltonetworks/terraform-provider-scm- Build the provider:
cd terraform-provider-scm
go build- Specify the
dev_overridesconfiguration per the next section below. This tells Terraform where to find the provider you just built. The directory to specify is the full path to the cloned provider repo.
With Terraform v1 and later, development overrides for provider developers can be leveraged in order to use the provider built from source.
To do this, populate a Terraform CLI configuration file (~/.terraformrc for all platforms other than Windows; terraform.rc in the %APPDATA% directory when using Windows) with at least the following options:
provider_installation {
dev_overrides {
"registry.terraform.io/paloaltonetworks-local/scm" = "/directory/containing/the/provider/binary/here"
}
direct {}
}Then when referencing the locally built provider, use the local name in the terraform configuration block like so:
terraform {
required_providers {
scm = {
source = "paloaltonetworks-local/scm"
version = "1.0.0"
}
}
}The Strata Cloud Manager authentication API has rate limits on token requests (approximately 10 concurrent requests per tenant). When running multiple concurrent Terraform operations (e.g., parallel CI/CD pipelines, multiple workspaces, or terraform apply with high parallelism), you may encounter authentication failures due to these rate limits.
Each provider instance requests a new JWT token during initialization, which can quickly exhaust the rate limit.
Starting from version 1.0.8, the Terraform provider supports passing pre-existing JWT tokens via the auth_file parameter. This allows you to implement a token caching solution that shares tokens across multiple Terraform runs.
You can configure the provider to use a cached token by specifying an auth_file that includes JWT fields:
provider "scm" {
auth_file = "/var/cache/scm/auth-token.json"
}The auth file should contain:
{
"client_id": "your-client-id",
"client_secret": "your-client-secret",
"host": "api.strata.paloaltonetworks.com",
"protocol": "https",
"scope": "tsg_id:1234567890",
"jwt": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"jwt_expires_at": "2026-01-21T10:30:00Z",
"jwt_lifetime": 900
}Important Security Note: Only share JWT tokens among Terraform runs that use the same client_id and client_secret. Different service principals with different RBAC permissions must never share tokens, as this would be a privilege escalation risk.
The provider does not include a built-in token caching service. Instead, you should implement your own caching mechanism that fits your infrastructure and security requirements.
┌─────────────────────────────────────────────────────────────────────────┐
│ Token Caching Architecture │
└─────────────────────────────────────────────────────────────────────────┘
┌──────────────────────┐
│ SCM Auth API │
│ (Rate Limited ~10 │
│ concurrent requests)│
└──────────┬───────────┘
│
│ 1. Fetch JWT Token
│ (Once every 10-12 min)
│
┌──────────▼───────────┐
│ Token Cache Service │
│ (Cron Job/Timer) │
│ │
│ • Checks expiration │
│ • Fetches new token │
│ • Updates auth file │
└──────────┬───────────┘
│
│ 2. Write (Atomic)
│ jwt + jwt_expires_at
│
┌───────────────▼────────────────┐
│ Shared Auth File │
│ /var/cache/scm/auth.json │
│ │
│ { │
│ "client_id": "...", │
│ "client_secret": "...", │
│ "jwt": "eyJ...", │
│ "jwt_expires_at": "...", │
│ "jwt_lifetime": 900 │
│ } │
└─┬────────┬────────┬────────┬───┘
│ │ │ │
3. Read JWT │ │ │ │ 3. Read JWT
(No API │ │ │ │ (No API
call) │ │ │ │ call)
│ │ │ │
┌─────────────▼──┐ ┌──▼────────▼──┐ ┌─▼─────────────┐
│ Terraform │ │ Terraform │ │ Terraform │
│ Process #1 │ │ Process #2 │ │ Process #N │
│ │ │ │ │ │
│ terraform │ │ terraform │ │ terraform │
│ apply │ │ apply │ │ apply │
│ (Workspace A) │ │ (Workspace B) │ (CI/CD Job) │
└─────────────────┘ └──────────────┘ └───────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ Benefits: │
│ • Only 1 auth request per tenant every 10-12 minutes │
│ • Supports unlimited concurrent Terraform processes │
│ • No rate limit errors during parallel operations │
│ • Cached token shared safely (read-only for Terraform processes) │
└─────────────────────────────────────────────────────────────────────────┘
How It Works:
-
Token Cache Service (cron job/systemd timer) runs every 10-12 minutes
- Checks if cached token is expired or expiring soon (60s buffer)
- Fetches new JWT token from SCM Auth API if needed
- Writes updated token to shared auth file (atomic write operation)
-
Shared Auth File (
/var/cache/scm/auth.jsonor similar)- Contains
client_id,client_secret, and cachedjwtfields - Updated atomically by token cache service
- Read by all Terraform provider instances
- Contains
-
Multiple Terraform Processes (concurrent operations)
- Each process reads the shared auth file on startup
- Uses cached JWT token (no API call needed)
- Can run unlimited concurrent operations without hitting rate limits
- All processes must use the same
client_id/client_secret
Disclaimer: The example code below is provided "as is" without warranty. It is intended as a reference implementation only. You are responsible for ensuring it meets your organization's security and operational requirements.
package main
import (
"context"
"encoding/json"
"log"
"os"
"time"
scm "github.com/paloaltonetworks/scm-go"
)
// Config represents the SCM configuration with JWT caching
type Config struct {
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
Host string `json:"host"`
Protocol string `json:"protocol"`
Scope string `json:"scope"`
JWT string `json:"jwt,omitempty"`
JWTExpiresAt time.Time `json:"jwt_expires_at,omitempty"`
JWTLifetime int64 `json:"jwt_lifetime,omitempty"`
AuthUrl string `json:"auth_url"`
}
func main() {
// Use absolute path for cron compatibility
configPath := os.Getenv("SCM_CONFIG_PATH")
if configPath == "" {
configPath := "/path/to/scm-config.json"
}
// Load existing config
config, err := loadConfig(configPath)
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
// Check if token needs refresh (5 minute buffer)
needsRefresh := config.JWT == "" || time.Now().After(config.JWTExpiresAt.Add(-300*time.Second))
if needsRefresh {
log.Println("Token expired or missing, fetching new token...")
// Create SCM client (will fetch new token)
client := &scm.Client{
ClientId: config.ClientID,
ClientSecret: config.ClientSecret,
Host: config.Host,
Protocol: config.Protocol,
Scope: config.Scope,
AuthUrl: config.AuthUrl,
CheckEnvironment: false, // Don't check env vars
}
if err := client.Setup(); err != nil {
log.Fatalf("Failed to setup client: %v", err)
}
// Get JWT token
if err := client.RefreshJwt(context.Background()); err != nil {
log.Fatalf("Failed to refresh JWT: %v", err)
}
// Update config with new token
config.JWT = client.Jwt
config.JWTExpiresAt = client.JwtExpiresAt
config.JWTLifetime = client.JwtLifetime
// Save updated config atomically
if err := saveConfigAtomic(configPath, config); err != nil {
log.Fatalf("Failed to save config: %v", err)
}
log.Printf("Token refreshed, expires at: %s\n", config.JWTExpiresAt)
} else {
log.Printf("Using cached token, expires at: %s\n", config.JWTExpiresAt)
}
}
func loadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var config Config
if err := json.Unmarshal(data, &config); err != nil {
return nil, err
}
return &config, nil
}
func saveConfigAtomic(path string, config *Config) error {
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
return err
}
// Write to temp file first
tmpPath := path + ".tmp"
if err := os.WriteFile(tmpPath, data, 0600); err != nil {
return err
}
// Atomic rename
return os.Rename(tmpPath, path)
}Usage: Run this as a cron job or systemd timer every 10-12 minutes to keep tokens fresh.
# Run every 10 minutes
*/10 * * * * /path/to/token-cache-service#!/usr/bin/env python3
"""
SCM Token Cache Service
Fetches and caches JWT tokens for concurrent SCM operations
"""
import json
import os
import sys
import tempfile
from datetime import datetime, timedelta
from pathlib import Path
from scm import Scm
def load_config(config_path: str) -> dict:
"""Load configuration from JSON file"""
with open(config_path, 'r') as f:
return json.load(f)
def save_config_atomic(config_path: str, config: dict) -> None:
"""Save configuration atomically with proper permissions"""
config_dir = os.path.dirname(config_path)
# Write to temporary file first
with tempfile.NamedTemporaryFile(
mode='w',
dir=config_dir,
delete=False,
prefix='.scm-config-',
suffix='.tmp'
) as tmp_file:
json.dump(config, tmp_file, indent=2)
tmp_path = tmp_file.name
# Set restrictive permissions
os.chmod(tmp_path, 0o600)
# Atomic rename
os.rename(tmp_path, config_path)
def needs_refresh(config: dict, buffer_seconds: int = 60) -> bool:
"""Check if token needs refresh"""
if not config.get('jwt') or not config.get('jwt_expires_at'):
return True
try:
expires_at = datetime.fromisoformat(
config['jwt_expires_at'].replace('Z', '+00:00')
)
# Refresh if expiring within buffer period
return datetime.now(expires_at.tzinfo) >= (
expires_at - timedelta(seconds=buffer_seconds)
)
except (ValueError, TypeError):
return True
def main():
config_path = os.path.expanduser("~/.scm/config.json")
# Ensure config directory exists
Path(config_path).parent.mkdir(parents=True, exist_ok=True, mode=0o700)
# Load config
try:
config = load_config(config_path)
except FileNotFoundError:
print(f"Error: Config file not found: {config_path}", file=sys.stderr)
sys.exit(1)
# Check if refresh needed
if needs_refresh(config):
print("Token expired or missing, fetching new token...")
# Create SCM client (will fetch new token)
client = Scm(
client_id=config.get('client_id'),
client_secret=config.get('client_secret'),
tsg_id=config.get('tsg_id') or config.get('scope', '').replace('tsg_id:', ''),
host=config.get('host', 'api.sase.paloaltonetworks.com'),
)
# Get token details
token = client.access_token
expires_at = client._token_expires_at
lifetime = client._jwt_lifetime
# Update config
config['jwt'] = token
config['jwt_expires_at'] = expires_at.isoformat() if expires_at else None
config['jwt_lifetime'] = lifetime
# Save atomically
save_config_atomic(config_path, config)
print(f"Token refreshed, expires at: {expires_at}")
else:
print(f"Using cached token, expires at: {config['jwt_expires_at']}")
if __name__ == '__main__':
main()Usage: Run this as a cron job every 10-12 minutes to keep tokens fresh.
# Run every 10 minutes
*/10 * * * * /usr/bin/python3 /path/to/token_cache_service.py#!/bin/bash
# Simple token cache updater using scm-go binary
CONFIG_FILE="${HOME}/.scm/scm-config.json"
GO_CACHE_TOOL="/path/to/scm-token-refresh" # Build from Example 1
# Check if token needs refresh
if [ -f "$CONFIG_FILE" ]; then
EXPIRES_AT=$(jq -r '.jwt_expires_at // empty' "$CONFIG_FILE")
if [ -n "$EXPIRES_AT" ]; then
EXPIRES_EPOCH=$(date -d "$EXPIRES_AT" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%S" "${EXPIRES_AT:0:19}" +%s 2>/dev/null)
NOW_EPOCH=$(date +%s)
BUFFER=120 # 2 minute buffer
if [ $((EXPIRES_EPOCH - NOW_EPOCH)) -gt $BUFFER ]; then
echo "Token still valid, expires at: $EXPIRES_AT"
exit 0
fi
fi
fi
echo "Refreshing token..."
"$GO_CACHE_TOOL"-
Token Lifecycle Management
- Refresh tokens before they expire (recommend 2-5 minute buffer)
- Use atomic file operations (write to temp, then rename)
- Handle token refresh failures gracefully
-
Security Considerations
- Store cache files with restrictive permissions (
chmod 600) - Use separate cache files for different credential sets
- Never commit auth files to version control
- Rotate credentials regularly
- Store cache files with restrictive permissions (
-
Concurrent Access
- Multiple processes can read the same cache file safely
- Only one process should write (use file locking if needed)
- Consider using a shared filesystem for distributed runners
Issue: Still getting authentication rate limit errors
- Verify all Terraform runs use the same cache file
- Check that token refresh logic is working
- Ensure cache file is readable by all processes
Issue: Different environments need different credentials
- Use separate cache files per credential set with provider aliases