envameleon 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 71067c7187ac66066961bb5ae52346bdfa7dd72a11b7506b25f728ac4c81a64d
4
+ data.tar.gz: d1dc5952b9fb0c501750c0655d905792b9bafe45a31c9a21af7f5ffe0b58cbdf
5
+ SHA512:
6
+ metadata.gz: b6492dd83fabd4f3d3e261bb4fcfa4fc16ee15c9d0c6f688b282b5a089dc4fff3512480904c32800ef6c9c7115e9c4d739f24ac2195587458b1a9d832feab392
7
+ data.tar.gz: c98a6dd250a633190cd3326a8f425d62902023e8855982db5430c33b7844a22a9e21e29586b221326a3070345910c84f5a8916ca5879f9149aa2cf1f76b62fef
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dmytro Shteflyuk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,237 @@
1
+ <a id="readme-top"></a>
2
+
3
+ <div align="center">
4
+ <img src="assets/envameleon.png" alt="ENVameleon — Leave no ENVidence." width="800">
5
+
6
+ <h1>ENVameleon</h1>
7
+ <p><strong>Leave no ENVidence.</strong></p>
8
+ <p>Hide the first process environment shown by Linux without changing Ruby's <code>ENV</code>.</p>
9
+ <p>
10
+ <a href="#about">About</a> ·
11
+ <a href="#getting-started">Get started</a> ·
12
+ <a href="#usage">Usage</a> ·
13
+ <a href="#how-it-works">How it works</a> ·
14
+ <a href="#development">Development</a>
15
+ </p>
16
+ </div>
17
+
18
+ ## Table of Contents
19
+
20
+ - [About](#about)
21
+ - [Getting Started](#getting-started)
22
+ - [Usage](#usage)
23
+ - [How It Works](#how-it-works)
24
+ - [Process Inheritance](#process-inheritance)
25
+ - [Security Limits](#security-limits)
26
+ - [Development](#development)
27
+ - [License](#license)
28
+
29
+ ## About
30
+
31
+ Linux exposes the environment passed at process start through
32
+ `/proc/self/environ`. ENVameleon lets a Ruby process mask, scrub, or drop that
33
+ view after boot. Ruby's live `ENV` stays intact.
34
+
35
+ ### Why this matters
36
+
37
+ Suppose your app keeps its credentials in an encrypted file. Your container
38
+ runner passes the unlock key through the process environment. The encrypted
39
+ file may seem safe even if an attacker can read any file that the app can
40
+ access.
41
+
42
+ Guess again. Linux also exposes `/proc/self/environ` as a file. It may hold the
43
+ unlock key passed at process launch. An attacker who reads both files can
44
+ decrypt the credentials, recover a session signing key, and forge sessions.
45
+
46
+ ENVameleon removes this copy of the key from the proc view after app boot.
47
+
48
+ The gem has no runtime dependencies. Loading it does not change anything. Your
49
+ app chooses when to call one of its three methods.
50
+
51
+ ## Getting Started
52
+
53
+ ### Requirements
54
+
55
+ - CRuby 3.2 or newer
56
+ - Linux for changes to `/proc/self/environ`
57
+ - The Linux `CAP_SYS_RESOURCE` right only for `ENV.drop_proc_data`
58
+
59
+ On other systems, all three methods are safe no-ops.
60
+
61
+ ### Installation
62
+
63
+ Install the gem:
64
+
65
+ ```console
66
+ gem install envameleon
67
+ ```
68
+
69
+ Or add it to your `Gemfile`:
70
+
71
+ ```ruby
72
+ gem "envameleon"
73
+ ```
74
+
75
+ Then load it:
76
+
77
+ ```ruby
78
+ require "envameleon"
79
+ ```
80
+
81
+ [Back to top](#readme-top).
82
+
83
+ ## Usage
84
+
85
+ ```ruby
86
+ require "envameleon"
87
+
88
+ ENV.mask_proc_data
89
+ # SECRET=example becomes SECRET=e*****e in /proc/self/environ
90
+
91
+ ENV.scrub_proc_data
92
+ # /proc/self/environ now holds only NUL bytes
93
+
94
+ ENV.drop_proc_data
95
+ # /proc/self/environ is now zero bytes long
96
+ ```
97
+
98
+ | Method | Result in `/proc/self/environ` | File length | Permissions |
99
+ | --- | --- | --- | --- |
100
+ | `ENV.mask_proc_data` | Names plus the first and last value byte | Unchanged | None |
101
+ | `ENV.scrub_proc_data` | NUL bytes | Unchanged | None |
102
+ | `ENV.drop_proc_data` | Empty | Zero | `CAP_SYS_RESOURCE` |
103
+
104
+ Choose the least power you need.
105
+
106
+ > [!IMPORTANT]
107
+ > `ENV.drop_proc_data` requires `CAP_SYS_RESOURCE` on Linux. Without it, the
108
+ > method raises `Errno::EPERM`.
109
+
110
+ Masking works on raw bytes, not characters. It replaces all value bytes except
111
+ the first and last with `*`. Values shorter than three bytes stay unchanged.
112
+
113
+ [Back to top](#readme-top).
114
+
115
+ ## How It Works
116
+
117
+ CRuby moves its active environment during startup. Linux still keeps the old
118
+ range used by `/proc/self/environ`. ENVameleon reads that range from
119
+ `/proc/self/stat`.
120
+
121
+ Masking and scrubbing first check that Ruby no longer uses the old range. They
122
+ then change those bytes in place. If Ruby still uses any of them, the method
123
+ raises an error and changes nothing.
124
+
125
+ Dropping uses `PR_SET_MM_ENV_END` to move the end of the kernel's view to its
126
+ start. This makes `/proc/self/environ` truly zero-length. Linux requires the
127
+ `CAP_SYS_RESOURCE` right for that call.
128
+
129
+ ## Process Inheritance
130
+
131
+ Call a method before `fork` and each child inherits the changed proc view. Ruby
132
+ `ENV` remains available in the parent and children. The test suite checks this
133
+ with a second fork for all three methods.
134
+
135
+ `spawn`, `system`, and `exec` are different. They give the new program a fresh
136
+ proc environment. That program must load ENVameleon and call a method itself.
137
+
138
+ ### Forking servers
139
+
140
+ You may call a method in the parent before it forks. You may also call it from a
141
+ worker boot hook. These examples use scrubbing, which needs no extra right.
142
+
143
+ #### Unicorn
144
+
145
+ ```ruby
146
+ require "envameleon"
147
+
148
+ after_fork do |_server, _worker|
149
+ ENV.scrub_proc_data
150
+ end
151
+ ```
152
+
153
+ #### Puma
154
+
155
+ Use `before_worker_boot` for cluster workers. In single mode, call the method
156
+ during app boot.
157
+
158
+ ```ruby
159
+ require "envameleon"
160
+
161
+ before_worker_boot do
162
+ ENV.scrub_proc_data
163
+ end
164
+ ```
165
+
166
+ #### Sidekiq
167
+
168
+ Standard Sidekiq uses threads. Its startup hook runs before work begins. Sidekiq
169
+ Enterprise Swarm runs the hook in each child.
170
+
171
+ ```ruby
172
+ require "envameleon"
173
+
174
+ Sidekiq.configure_server do |config|
175
+ config.on(:startup) { ENV.scrub_proc_data }
176
+ end
177
+ ```
178
+
179
+ #### Karafka
180
+
181
+ The `app.running` event runs in the server process. In Swarm mode, it runs in
182
+ each forked node.
183
+
184
+ ```ruby
185
+ require "envameleon"
186
+
187
+ Karafka::App.monitor.subscribe("app.running") do
188
+ ENV.scrub_proc_data
189
+ end
190
+ ```
191
+
192
+ If a child calls `ENV.drop_proc_data`, it must still have the needed Linux right.
193
+
194
+ [Back to top](#readme-top).
195
+
196
+ ## Security Limits
197
+
198
+ > [!WARNING]
199
+ > **`CAP_SYS_RESOURCE` grants broad powers outside ENVameleon.**
200
+ >
201
+ > `ENV.drop_proc_data` needs this Linux capability. Grant it only if you accept
202
+ > its wider access. Masking and scrubbing do not need it.
203
+
204
+ ENVameleon changes only what `/proc/self/environ` shows. It does not erase every
205
+ copy of a secret. It gives no protection from memory disclosure, debuggers, or
206
+ core dumps.
207
+
208
+ Dropping leaves the old bytes in memory. Masking keeps variable names and the
209
+ outer bytes of each value. Short values remain fully visible. Scrubbing
210
+ overwrites the old proc range, but a secret may still exist elsewhere.
211
+
212
+ [Back to top](#readme-top).
213
+
214
+ ## Development
215
+
216
+ Build the native extension and run the `test/unit` suite:
217
+
218
+ ```console
219
+ ruby -Cext/envameleon extconf.rb
220
+ make -C ext/envameleon
221
+ ruby -Ilib -Iext test/test_envameleon.rb
222
+ ```
223
+
224
+ On Linux, the drop test runs when `CAP_SYS_RESOURCE` is available. Otherwise,
225
+ that one test is omitted.
226
+
227
+ Build the gem with:
228
+
229
+ ```console
230
+ gem build envameleon.gemspec
231
+ ```
232
+
233
+ ## License
234
+
235
+ Distributed under the MIT License. See `LICENSE.txt`.
236
+
237
+ [Back to top](#readme-top).
Binary file
@@ -0,0 +1,166 @@
1
+ #include <ruby.h>
2
+
3
+ #ifdef __linux__
4
+ #include <errno.h>
5
+ #include <stdint.h>
6
+ #include <stdio.h>
7
+ #include <stdlib.h>
8
+ #include <string.h>
9
+ #include <sys/prctl.h>
10
+ #include <linux/prctl.h>
11
+
12
+ static void
13
+ read_env_range(unsigned long *env_start, unsigned long *env_end)
14
+ {
15
+ char stat[4096];
16
+ char *field;
17
+ char *end;
18
+ FILE *file = fopen("/proc/self/stat", "r");
19
+ int number;
20
+
21
+ if (file == NULL)
22
+ rb_sys_fail("fopen(/proc/self/stat)");
23
+
24
+ if (fgets(stat, sizeof(stat), file) == NULL) {
25
+ int error = errno;
26
+ fclose(file);
27
+ if (error != 0) {
28
+ errno = error;
29
+ rb_sys_fail("fgets(/proc/self/stat)");
30
+ }
31
+ rb_raise(rb_eRuntimeError, "empty /proc/self/stat");
32
+ }
33
+ fclose(file);
34
+
35
+ field = strrchr(stat, ')');
36
+ if (field == NULL || field[1] != ' ')
37
+ rb_raise(rb_eRuntimeError, "invalid /proc/self/stat");
38
+ field += 2;
39
+
40
+ for (number = 3; number < 50; number++) {
41
+ field = strchr(field, ' ');
42
+ if (field == NULL)
43
+ rb_raise(rb_eRuntimeError, "missing env_start in /proc/self/stat");
44
+ field++;
45
+ }
46
+
47
+ errno = 0;
48
+ *env_start = strtoul(field, &end, 10);
49
+ if (errno != 0 || end == field || *env_start == 0)
50
+ rb_raise(rb_eRuntimeError, "invalid env_start in /proc/self/stat");
51
+
52
+ field = end;
53
+ errno = 0;
54
+ *env_end = strtoul(field, &end, 10);
55
+ if (errno != 0 || end == field || *env_end < *env_start)
56
+ rb_raise(rb_eRuntimeError, "invalid env_end in /proc/self/stat");
57
+ }
58
+
59
+ static void
60
+ ensure_environment_is_detached(unsigned long env_start, unsigned long env_end)
61
+ {
62
+ extern char **environ;
63
+ char **entry;
64
+
65
+ for (entry = environ; entry != NULL && *entry != NULL; entry++) {
66
+ uintptr_t address = (uintptr_t)*entry;
67
+
68
+ if (address >= env_start && address < env_end)
69
+ rb_raise(rb_eRuntimeError, "Ruby ENV overlaps /proc/self/environ");
70
+ }
71
+ }
72
+
73
+ static VALUE
74
+ scrub_proc_data(VALUE environment)
75
+ {
76
+ unsigned long env_start;
77
+ unsigned long env_end;
78
+
79
+ (void)environment;
80
+ read_env_range(&env_start, &env_end);
81
+ ensure_environment_is_detached(env_start, env_end);
82
+ memset((void *)env_start, 0, env_end - env_start);
83
+ return Qnil;
84
+ }
85
+
86
+ static VALUE
87
+ mask_proc_data(VALUE environment)
88
+ {
89
+ unsigned long env_start;
90
+ unsigned long env_end;
91
+ char *entry;
92
+
93
+ (void)environment;
94
+ read_env_range(&env_start, &env_end);
95
+ ensure_environment_is_detached(env_start, env_end);
96
+
97
+ for (entry = (char *)env_start; entry < (char *)env_end;) {
98
+ size_t remaining = (char *)env_end - entry;
99
+ char *terminator = memchr(entry, '\0', remaining);
100
+ char *equals;
101
+ char *value;
102
+ size_t value_length;
103
+
104
+ if (terminator == NULL)
105
+ rb_raise(rb_eRuntimeError, "invalid environment data");
106
+
107
+ equals = memchr(entry, '=', terminator - entry);
108
+ if (equals != NULL) {
109
+ value = equals + 1;
110
+ value_length = terminator - value;
111
+ if (value_length > 2)
112
+ memset(value + 1, '*', value_length - 2);
113
+ }
114
+
115
+ entry = terminator + 1;
116
+ }
117
+
118
+ return Qnil;
119
+ }
120
+
121
+ static VALUE
122
+ drop_proc_data(VALUE environment)
123
+ {
124
+ unsigned long env_start;
125
+ unsigned long env_end;
126
+
127
+ (void)environment;
128
+ read_env_range(&env_start, &env_end);
129
+ if (prctl(PR_SET_MM, PR_SET_MM_ENV_END, env_start, 0L, 0L) == -1)
130
+ rb_sys_fail("prctl(PR_SET_MM_ENV_END)");
131
+
132
+ return Qnil;
133
+ }
134
+ #else
135
+ static VALUE
136
+ scrub_proc_data(VALUE environment)
137
+ {
138
+ (void)environment;
139
+ return Qnil;
140
+ }
141
+
142
+ static VALUE
143
+ mask_proc_data(VALUE environment)
144
+ {
145
+ (void)environment;
146
+ return Qnil;
147
+ }
148
+
149
+ static VALUE
150
+ drop_proc_data(VALUE environment)
151
+ {
152
+ (void)environment;
153
+ return Qnil;
154
+ }
155
+ #endif
156
+
157
+ void
158
+ Init_envameleon(void)
159
+ {
160
+ ID environment_id = rb_intern2("ENV", 3);
161
+ VALUE environment = rb_const_get(rb_cObject, environment_id);
162
+
163
+ rb_define_singleton_method(environment, "scrub_proc_data", scrub_proc_data, 0);
164
+ rb_define_singleton_method(environment, "mask_proc_data", mask_proc_data, 0);
165
+ rb_define_singleton_method(environment, "drop_proc_data", drop_proc_data, 0);
166
+ }
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+
5
+ create_makefile("envameleon/envameleon")
data/lib/envameleon.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ENVameleon
4
+ VERSION = "0.1.0"
5
+ end
6
+
7
+ require "envameleon/envameleon"
metadata ADDED
@@ -0,0 +1,45 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: envameleon
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Dmytro Shteflyuk
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Scrub, mask, or drop Linux's /proc/self/environ view without changing
13
+ Ruby's ENV.
14
+ executables: []
15
+ extensions:
16
+ - ext/envameleon/extconf.rb
17
+ extra_rdoc_files: []
18
+ files:
19
+ - LICENSE.txt
20
+ - README.md
21
+ - assets/envameleon.png
22
+ - ext/envameleon/envameleon.c
23
+ - ext/envameleon/extconf.rb
24
+ - lib/envameleon.rb
25
+ licenses:
26
+ - MIT
27
+ metadata: {}
28
+ rdoc_options: []
29
+ require_paths:
30
+ - lib
31
+ required_ruby_version: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: '3.2'
36
+ required_rubygems_version: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ requirements: []
42
+ rubygems_version: 3.6.9
43
+ specification_version: 4
44
+ summary: Leave no ENVidence.
45
+ test_files: []