SAS Cloud Analytic Services (CAS) can do more than run native CAS actions, it can also invoke external languages like Python and R directly from a CAS session, through actions such as dataStep.runCode or the python/R language extensions. This is a powerful capability: it lets data scientists blend open-source code with distributed, in-memory CAS processing.
Configuring extlang.xml is a core step when setting up Python and R integration in SAS Viya, serving as the primary control point for governing user access to external language interpreters. This Kubernetes-native deployment method applies seamlessly to both brand-new SAS Viya 4 environments and existing running clusters.
Powerful capabilities like that need guardrails. By default, CAS runs as a shared service account, so allowing any authenticated user to execute arbitrary Python or R code inside a CAS session is effectively allowing arbitrary code execution under that service identity. SAS Viya addresses this with a single, declarative control point: the extlang.xml file.
This post explains what extlang.xml does, how its structure enforces a security model, and walks through a practical, Kubernetes-native way to create and deploy it in a SAS Viya 4 deployment.
Without an explicit policy, a CAS deployment has to make an all-or-nothing decision about external language execution: either no one can run Python/R from CAS, or everyone can. Neither extreme fits a real environment, where you typically want to:
extlang.xml gives CAS a single, auditable file that encodes exactly this policy, evaluated per user and per group, before any external language process is launched.
CAS locates its policy file through the SAS_EXTLANG_SETTINGS environment variable, which points to a path such as:
/opt/sas/viya/home/sas-pyconfig/extlang.xml
That path needs to live somewhere CAS can actually read, which is why SAS Viya deployments typically dedicate a persistent volume claim (PVC), often named sas-pyconfig, to hold both the external language runtimes (Python, R) and this policy file.
Before creating or changing the file, confirm that your deployment already uses the target environment variable by inspecting the CAS controller pod with kubectl exec:
CAS_POD=sas-cas-server-default-controller
CONTAINER=sas-cas-server
SETTINGS_PATH=$(kubectl -n gelenv exec "$CAS_POD" -c "$CONTAINER" -- printenv SAS_EXTLANG_SETTINGS)
echo "SAS_EXTLANG_SETTINGS = ${SETTINGS_PATH}"
If this returns nothing, the environment variable isn't set for that pod, and CAS won't enforce any extlang.xml policy at all; external language access falls back to whatever CAS's built-in default is. The path shown above /opt/sas/viya/home/sas-pyconfig/extlang.xml is only the common default; your deployment may point SAS_EXTLANG_SETTINGS at a different directory, and that directory may be backed by a PVC with a different name than sas-pyconfig. Don't assume either, confirm both before you author or copy anything.
Two things are worth calling out about the access model:
The extlang.xml file is organized into a top-level <EXTLANG> wrapper containing a <DEFAULT> baseline block and optional <GROUP> override blocks.
Global Baseline: <EXTLANG> -> <DEFAULT> -> <LANGUAGE>
Group Overrides: <EXTLANG> -> <GROUP> -> <LANGUAGE>
Important: despite its name, <GROUP> has nothing to do with SAS Viya custom groups, LDAP groups, or Active Directory groups. It is purely a local policy label inside this XML file, you can call it anything you like. The users attribute is what ties the rule to actual users, listed by their host account names. If a <GROUP> block says name="analysts" users="Henrik,Delilah", CAS doesn't look up an "analysts" group anywhere; it simply applies that block's permissions to Henrik and Delilah individually.
Tip: To see a complete, working extlang.xml file before diving into the individual settings, jump down to the “kubectl cp into a disposable utility pod” section below.
The file is organized into a <DEFAULT> block and one or more <GROUP> blocks:
| Element / Attribute | Purpose |
| <EXTLANG mode, allowAllUsers> | Global switch and default-deny posture for the whole file |
| <DEFAULT> | Baseline settings applied to every session before group-level rules are layered on |
| scratchDisk | Directory external language processes use for temporary files |
| diskAllowlist | Paths the external process is permitted to read/write beyond the scratch disk |
| userSetScratchDisk | Whether an end user can override the scratch disk (ALLOW/BLOCK) |
| <LANGUAGE name="PYTHON3"\ > | Per-language configuration, nested inside <DEFAULT> or <GROUP> |
| interpreter | Full path to the interpreter binary CAS should launch |
| userSetEnv | Whether users can set their own environment variables for the language process |
| userSetInterpreter | Whether users can point to a different interpreter than the configured one |
| userInlineCode | Whether users can submit inline Python/R code (as opposed to only pre-approved scripts) |
| <GROUP name="..." users="..."> | Logical label for a policy block; the users attribute ties it to named users, layered on top of <DEFAULT> |
The evaluation flow is additive: <DEFAULT> establishes the locked-down baseline (fixed interpreter, no user overrides), and each <GROUP> block loosens specific permissions for its members, never the reverse. This is a classic least-privilege pattern: start closed, open up deliberately for named groups.
extlang.xml just needs to land at whatever path SAS_EXTLANG_SETTINGS points to, on whatever PVC backs that path, which won't necessarily be /opt/sas/viya/home/sas-pyconfig on a PVC named sas-pyconfig, even though that's the common default shown in SAS documentation. Before writing anything, resolve the real mount path and PVC name for your deployment directly from $SETTINGS_PATH. To do this, you will “get” the PVC and volume mounts information looking at the “sas-cas-server” definition using kubectl:
MOUNT_PATH=$(dirname "$SETTINGS_PATH")
VOLUME_NAME=$(kubectl -n gelenv get pod "$CAS_POD" -o jsonpath='{.spec.containers[?(@.name=="'"$CONTAINER"'")].volumeMounts[?(@.mountPath=="'"$MOUNT_PATH"'")].name}')
PVC_NAME=$(kubectl -n gelenv get pod "$CAS_POD" -o jsonpath='{.spec.volumes[?(@.name=="'"$VOLUME_NAME"'")].persistentVolumeClaim.claimName}')
echo "MOUNT_PATH = ${MOUNT_PATH}"
echo "PVC_NAME = ${PVC_NAME}"
This takes the directory portion of $SETTINGS_PATH as MOUNT_PATH (true as long as extlang.xml sits directly at the volume's mount point, which is the standard layout) then looks up the volume mounted at exactly that path and resolves its PVC claim name. If your deployment nests the file under a subdirectory of the actual mount (uncommon), you'll need to recursively check parent directories to find the real mount point instead.
This walks the CAS container's volumeMounts, finds the one whose path is a prefix of $SETTINGS_PATH (the longest match wins, in case one mount is nested inside another), then resolves that volume's actual PVC claim name. MOUNT_PATH and PVC_NAME are now set correctly for your environment, whatever it's called, and the rest of this walkthrough uses them instead of hardcoded defaults.
It's tempting to kubectl cp straight into the CAS controller pod, since it already has the PVC mounted, but that mount is deliberately read-only on the CAS side (CAS shouldn't be able to rewrite its own interpreter/policy volume at runtime), so the copy fails:
tar: extlang.xml: Cannot open: Read-only file system
command terminated with exit code 2
The fix is to copy into a small, disposable pod that mounts the same PVC read-write instead, then delete it once the file is in place.
The SAS administrator should author extlang.xml as a plain, standalone file; editable, diffable, and committable in version control just like any other config. Here's a starting point you can copy, save locally as extlang.xml, and adjust to match your own users, groups, and, if MOUNT_PATH differs from the default, your own interpreter and scratch disk paths:
<EXTLANG version="1.0" mode="ALLOW" allowAllUsers="BLOCK">
<DEFAULT scratchDisk="/tmp"
diskAllowlist="/opt/sas/viya/home/sas-pyconfig"
userSetScratchDisk="BLOCK">
<LANGUAGE name="PYTHON3"
interpreter="/opt/sas/viya/home/sas-pyconfig/default_py/bin/python3"
userSetEnv="BLOCK"
userSetInterpreter="BLOCK" />
<LANGUAGE name="R"
interpreter="/opt/sas/viya/home/sas-pyconfig/default_r/bin/Rscript"
userSetEnv="BLOCK"
userSetInterpreter="BLOCK" />
</DEFAULT>
<GROUP name="PowerUsers" users="sasadm">
<LANGUAGE name="PYTHON3"
userInlineCode="ALLOW"
userSetEnv="ALLOW"
userSetInterpreter="ALLOW" />
<LANGUAGE name="R"
userInlineCode="ALLOW"
userSetEnv="ALLOW"
userSetInterpreter="ALLOW" />
</GROUP>
<GROUP name="analysts" users="Henrik,Delilah">
<LANGUAGE name="PYTHON3" userInlineCode="ALLOW" />
<LANGUAGE name="R" userInlineCode="ALLOW" />
</GROUP>
</EXTLANG>
This example locks the interpreter and scratch disk for everyone, then loosens permissions for two named groups: PowerUsers gets full control over environment and interpreter selection, while analysts (named users Henrik and Delilah) can only submit inline code with the fixed, locked-down interpreter. Adjust the group names, users, and languages to match your own environment.
Note that PowerUsers and analysts above are arbitrary labels chosen for readability, they are not looked up in Viya, LDAP, or Active Directory. Only the users attribute matters for access control.
Next, launch a minimal pod that mounts $PVC_NAME read-write at $MOUNT_PATH. It doesn't need to run anything beyond staying alive long enough to receive the file (the sleep 3600 keeps it active for 1 hour) :
kubectl -n gelenv delete pod debug-sas-pyconfig --ignore-not-found
kubectl -n gelenv apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: sas-pyconfig-writer
spec:
securityContext:
fsGroup: 1001
runAsGroup: 1001
runAsNonRoot: true
runAsUser: 1001
seccompProfile:
type: RuntimeDefault
restartPolicy: Never
containers:
- name: alpine
image: alpine:3.20
command: ["sleep", "3600"]
volumeMounts:
- name: pyconfig
mountPath: ${MOUNT_PATH}
volumes:
- name: pyconfig
persistentVolumeClaim:
claimName: ${PVC_NAME}
EOF
Copy the file into the new pod, which in turn writes it directly to the mounted persistent volume. Once the file is written, remove the pod, it was only a temporary vehicle for writing the file, not a long-running workload.
# extlang.xml is the file you just saved locally, e.g. checked into your Viya config repo
kubectl -n gelenv cp ./extlang.xml "sas-pyconfig-writer:${MOUNT_PATH}/extlang.xml"
kubectl -n gelenv delete pod sas-pyconfig-writer
Confirm the file landed correctly and with the right ownership:
kubectl -n gelenv exec "$CAS_POD" -c "$CONTAINER" -- ls -alF "$MOUNT_PATH"
You should see extlang.xml alongside the default_py / default_r interpreter symlinks already provisioned on the volume. Reading from the CAS controller's read-only mount works fine; only writes to it are blocked. New and existing CAS sessions will pick up the new or updated policy.
Find more articles from SAS Global Enablement and Learning here.
Visit the Tips & Tricks page for setup guidance, demos, and practical examples that show how Copilot supports your workflows.
The rapid growth of AI technologies is driving an AI skills gap and demand for AI talent. Ready to grow your AI literacy? SAS offers free ways to get started for beginners, business leaders, and analytics professionals of all skill levels. Your future self will thank you.