Environment variables decide where a shell writes its history, which libraries a program loads and, in most pipelines, which credentials a process holds, yet they are usually writable by anyone who controls the session. M1039 Environment Variable Permissions is the MITRE ATT&CK mitigation that restricts who can change them. Its formal mapping in ATT&CK v20 is narrow, two techniques about command history, but the control it describes reaches further: configuration file permissions, CI/CD variable access and process scoping. This guide covers what ATT&CK maps, how to implement each measure, the closely related techniques ATT&CK assigns to other mitigations, and how the same discipline protects secrets held in environment variables. It is for Linux, platform and DevSecOps engineers and for detection teams. The basics of handling .env files are in the linked guide and are not repeated here.
Key takeaways
- The mapping is small but specific. ATT&CK v20 maps M1039 to T1690 Prevent Command History Logging and T1070.003 Clear Command History, both about protecting HISTFILE, HISTCONTROL and HISTFILESIZE.
- The measures are broader than the mapping. ATT&CK's description covers file permissions on shell and systemd configuration, RBAC on CI/CD variables, process scoping in containers and auditing changes.
- Hijacking variables sit next door. LD_PRELOAD (T1574.006), PATH (T1574.007) and COR_PROFILER (T1574.012) abuse is mapped by ATT&CK to other mitigations, but locking down the same configuration files helps against them.
- Secrets in variables are an identity problem. Every process in a job or container inherits the environment, so a token in an environment variable is exposed to every script that runs there.
- Read-only history is a speed bump. It raises the cost of hiding commands, but forwarding command logs off the host is what makes them reliable evidence.
What M1039 covers
ATT&CK describes M1039 as restricting the modification of environment variables to authorised users and processes through strict permissions and policies (attack.mitre.org, mitigation version 1.1, last modified 11 December 2024). Its four measures, with ATT&CK's own examples:
| Measure in ATT&CK text | ATT&CK use case and example | Where it applies |
|---|---|---|
| Restrict write access | File permissions on .bashrc, .bash_profile, .zshrc and systemd service files; only root or administrators may modify /etc/environment or /etc/profile | Linux and macOS hosts |
| Secure access controls | RBAC in CI/CD tools such as Jenkins or GitLab so only specific users can modify variables | Pipelines and deployment tools |
| Restrict process scope | Use containers to isolate variables to specific containers so other processes do not inherit them | Containers and orchestration |
| Audit changes | Use auditd to monitor changes to /etc/environment and application environment files | Linux hosts |
Techniques M1039 mitigates
| Tactic | Technique | How M1039 helps (per ATT&CK usage text) |
|---|---|---|
| Defense Impairment | T1690 Prevent Command History Logging | Prevent users from changing the HISTCONTROL, HISTFILE and HISTFILESIZE environment variables |
| Stealth | T1070.003 Clear Command History | Making the history-related variables read-only may ensure history is preserved |
Both techniques rely on Bash behaviour documented in the GNU Bash manual: if HISTFILE is unset or null, the shell does not save history on exit; HISTSIZE set to 0 stops commands being saved to the history list; HISTFILESIZE set to 0 truncates the history file; and HISTCONTROL values such as ignorespace skip lines that begin with a space. An attacker who can change any of these hides commands without deleting a file. See the T1070 Indicator Removal Defensive Guide.
Related techniques mapped to other mitigations
ATT&CK v20 does not map these to M1039, but they abuse environment variables, and the file permissions M1039 describes reduce the places an attacker can set them. The mitigations listed are ATT&CK's own.
| Technique | Variable abused | ATT&CK mitigations |
|---|---|---|
| T1574.006 Dynamic Linker Hijacking | LD_PRELOAD and similar loader variables | M1028 Operating System Configuration, M1038 Execution Prevention |
| T1574.007 Path Interception by PATH Environment Variable | PATH | M1022 Restrict File and Directory Permissions, M1038, M1047 Audit |
| T1574.012 COR_PROFILER | COR_PROFILER and related .NET profiler variables | M1018 User Account Management, M1024 Restrict Registry Permissions, M1038 |
| T1546.004 Unix Shell Configuration Modification | Shell startup files that set variables and run commands | M1022 Restrict File and Directory Permissions |
See the T1574 Hijack Execution Flow Defensive Guide and T1546 Event Triggered Execution Defensive Guide.
Implementing M1039 on hosts
- System-wide files: /etc/environment, /etc/profile, /etc/profile.d/*, /etc/bash.bashrc and systemd unit files (including
Environment=andEnvironmentFile=targets) should be owned by root and not writable by group or others. Check them in configuration management, not by hand. - History variables: NHI Mgmt Group recommends setting HISTFILE, HISTSIZE, HISTFILESIZE and HISTCONTROL in a root-owned file under /etc/profile.d and marking them with the Bash
readonlybuiltin, so that an interactive login session cannot reassign or unset them. This is the measure ATT&CK's usage text describes. - User startup files: On shared jump hosts and bastions, make users' .bashrc and .bash_profile root-owned or managed, because a user-writable startup file can undo system settings before the prompt appears.
- Service accounts: Service accounts that need no shell should have a non-login shell; those that do should not be able to write their own startup files.
- Windows: System environment variables live in the registry under HKLM and require administrative rights to change; per-user variables under HKCU do not. Restrict and monitor the HKLM location, which links M1039 to M1024 Restrict Registry Permissions.
Environment variables, pipelines and secrets
For non-human identities the bigger risk is not that a variable is changed but that it is read. A process's environment is inherited by every child process, and on Linux the initial environment of a running program is exposed at /proc/<pid>/environ, with access governed by a ptrace read-access check. The M1039 measures still apply, extended by NHI Mgmt Group recommendations:
- Secure access controls in CI/CD: GitLab documents that project variables need the Maintainer role, group variables the Owner role, and instance variables administrator access. Protected variables reach only pipelines on protected branches or tags. Masked variables are replaced with
[MASKED]in job logs, but GitLab states masking does not stop a malicious job reading the value, and that anyone who can push changes to.gitlab-ci.ymlcould expose variables. Treat the right to edit pipeline definitions as access to every variable the pipeline can reach. - Restrict process scope: Kubernetes guidance says that when only one container in a pod needs a Secret, the volume mount or environment variable configuration should be defined so other containers do not get it, and that applications must avoid logging secret data after reading it. Prefer mounted files or file-type variables (GitLab stores the value in a temporary file and puts the path in the variable) for high-value credentials, and give each step only the variables it needs.
- Short-lived credentials: A workload identity token that expires in minutes is far less valuable in a leaked environment than a long-lived API key.
The Codecov breach shows why scope matters: a modified uploader script, run inside customers' CI jobs, captured the full set of environment variables, including tokens and cloud keys, and sent them to an attacker-controlled server. Every secret in the job's environment was exposed to a third-party script that needed none of them. The 230 million AWS cloud environments campaign shows the file side of the same problem: .env files served by misconfigured web applications yielded more than 90,000 unique variables, including AWS access keys and GitHub tokens. See the Environment Variables and .env Files guide and How to find secrets in Lambda environment variables.
Choosing the approach
| Need | Approach | Trade-off |
|---|---|---|
| Preserve shell history on servers | Read-only history variables plus off-host command logging | Read-only alone is bypassed by other shells or interpreters |
| Stop tampering with system-wide variables | Root-owned configuration files, managed by configuration management, with file integrity monitoring | Changes need a deployment rather than a quick edit |
| Protect pipeline secrets | Protected, masked or hidden variables; minimum roles to edit pipelines; per-step scoping | Masking hides logs, not the value from the job |
| Protect container secrets | Mounted secret files per container, external secret store, short-lived tokens | Applications may need changes to read files |
Small teams get most value from two moves: root-owned system configuration and restricting who can edit pipeline definitions. Larger estates should add file integrity monitoring on the configuration files and a secrets manager so long-lived credentials leave environment variables altogether.
Implementation approach
- Inventory. List system-wide environment files, systemd units with environment settings, and every CI/CD variable with its scope, protection flag and owner.
- Fix permissions. Apply root ownership and remove group and world write on system files through configuration management, starting with bastions and production servers.
- Pilot read-only history. Deploy the /etc/profile.d settings to a pilot group of servers and confirm administrators' tooling still works.
- Tighten pipeline access. Move secrets to protected variables, require merge request review for pipeline file changes, and remove secrets that no job uses.
- Roll back safely. Keep configuration changes in version control so a breaking change is reverted by redeploying the previous version.
What M1039 does not stop
- Other interpreters and shells: Read-only Bash variables do nothing for commands run through Python, a different shell or a non-interactive session that does not read the profile.
- Root: An attacker with root can edit the files, kill the shell before history is flushed, or delete the history file (T1070.003). Off-host logging (M1029 Remote Data Storage) is the counter.
- Reading secrets: Permission on who can change a variable does not limit who can read it; process scoping and secret stores do that.
- Malicious code inside the job: As the Codecov case shows, code running in the pipeline sees the pipeline's environment regardless of RBAC on the settings page.
Common failure patterns
- History treated as evidence. Investigations rely on ~/.bash_history, which the attacker controls, instead of logs forwarded off the host.
- World-writable profile scripts. A script under /etc/profile.d deployed with loose permissions lets any user set variables for every login.
- Global CI variables. Cloud keys defined at group or instance level reach every project and every job, including those that never need them.
- Masking mistaken for protection. Teams assume a masked variable is safe from scripts in the job.
- Secrets in images and manifests. Values baked into container images or committed in manifests, readable by anyone with repository or registry access.
Detection and measurement
- Audit file changes: watch /etc/environment, /etc/profile, /etc/profile.d and systemd unit directories with auditd. The auditctl manual notes that the
-wwatch form is deprecated for performance and gives the syscall form, for example-a always,exit -F arch=b64 -F path=/etc/environment -F perm=wa, with a key forausearch. - Detect history tampering: alert on commands that unset or reassign HISTFILE, set HISTSIZE or HISTFILESIZE to 0, or truncate history files.
- Pipeline audit: log changes to CI/CD variables and to pipeline definition files, and alert on new unprotected variables holding credentials.
| Metric | What good looks like |
|---|---|
| System environment files with correct ownership and mode | 100% of managed Linux hosts |
| Servers forwarding command logs off-host | All production and bastion hosts |
| CI/CD credentials stored as protected variables | All production credentials |
| Long-lived secrets in environment variables | Falling quarter on quarter as workloads move to short-lived tokens |
| History tampering alerts reviewed within SLA | All |
D3FEND and related mitigations
The MITRE D3FEND knowledge graph relates M1039 to D3-ACH Application Configuration Hardening (modifying an application's configuration to reduce its attack surface) and D3-SFA System File Analysis (monitoring system files such as configuration files for modification or tampering). These match the restrict-write and audit measures above.
| Related mitigation | Relationship to M1039 |
|---|---|
| M1022 Restrict File and Directory Permissions | Protects the files that set variables |
| M1028 Operating System Configuration | Also mapped to T1690; system-level history and loader settings |
| M1029 Remote Data Storage | Keeps logs and history off the host, mapped to T1070.003 |
| M1038 Execution Prevention | Blocks payloads loaded through PATH or LD_PRELOAD abuse |
See M1022 Restrict File and Directory Permissions, M1029 Remote Data Storage and M1038 Execution Prevention.
Framework mapping
| Framework | Reference | Relevance |
|---|---|---|
| NIST SP 800-53 Rev. 5 | CM-6 Configuration Settings; AC-6 Least Privilege | Locked configuration and minimal rights to change it |
| NIST SP 800-53 Rev. 5 | AU-9 Protection of Audit Information; SI-7 Software, Firmware, and Information Integrity | Protecting history and detecting configuration tampering |
| NIST CSF 2.0 | PR.PS-01; PR.AA-05 | Configuration management; least-privilege access |
| CIS Controls v8.1 | 4.1 Secure Configuration Process; 8.9 Centralize Audit Logs | Managed configuration and off-host logs |
| ISO/IEC 27001:2022 Annex A | 8.9 Configuration management; 8.15 Logging | Configuration control and log protection |
Practitioner checklist
- Make /etc/environment, /etc/profile, /etc/profile.d and systemd environment files root-owned and not group or world writable.
- Set history variables read-only on servers and bastions through a managed /etc/profile.d file.
- Forward command and audit logs off the host so history is not the only record.
- Audit changes to environment configuration files with syscall-form auditd rules.
- Restrict who can edit CI/CD variables and pipeline definitions; review pipeline changes before they run.
- Scope secrets to the jobs, steps and containers that need them; prefer mounted files for high-value credentials.
- Replace long-lived secrets in environment variables with short-lived workload credentials.
- Alert on HISTFILE, HISTSIZE or HISTFILESIZE tampering.
Standards and references
- MITRE ATT&CK: M1039 Environment Variable Permissions
- MITRE D3FEND: Application Configuration Hardening (D3-ACH)
- GNU: Bash Reference Manual, Bash Variables
- Linux man-pages: auditctl(8)
- Linux man-pages: proc_pid_environ(5)
- GitLab Docs: GitLab CI/CD variables
- Kubernetes: Good practices for Kubernetes Secrets
Related NHI Mgmt Group resources: Environment Variables and .env Files · How to find secrets in Lambda environment variables · T1070 Indicator Removal Defensive Guide · M1037 Filter Network Traffic