Class: RKSeal::SecureWorkspace
- Inherits:
-
Object
- Object
- RKSeal::SecureWorkspace
- Defined in:
- lib/rkseal/secure_workspace.rb
Overview
Provides a RAM-backed scratch path for the plaintext edit buffer and guarantees its destruction. This is the single enforcement point for the hard rule: **plaintext must never touch persistent disk.**
The medium is chosen per-OS behind one interface:
- Linux: a tmpfs path (`/dev/shm`, or `$XDG_RUNTIME_DIR`).
- macOS: an ephemeral `hdiutil`-backed RAM disk, attached for the duration
of the edit and detached afterwards (macOS has no tmpfs/`/dev/shm`).
There is **no on-disk ‘mktemp` fallback**. If a RAM-backed medium cannot be provisioned, the workspace raises WorkspaceError rather than degrade the security guarantee.
The public API is block-scoped so callers cannot forget teardown: the path exists only inside the block, and on block exit (normal, exception, or signal) the file is best-effort shredded/overwritten and unlinked and any RAM disk is detached. Signal handling and ‘at_exit` registration guard against a crash leaking a mounted RAM disk. Secret values are never logged.
rubocop:disable Metrics/ClassLength – this single class deliberately holds the workspace orchestration plus its two tightly-coupled per-OS medium strategies (LinuxMedium, MacosMedium). They are one cohesive unit and the gem keeps one layer per file, so splitting them out would scatter the “never on disk” guarantee rather than clarify it.
Defined Under Namespace
Classes: LinuxMedium, MacosMedium
Constant Summary collapse
- FILE_MODE =
Filesystem permissions for the scratch file: owner read/write only.
0o600- DIR_MODE =
Permissions for the (Linux) scratch directory: owner only.
0o700- RAM_DISK_BYTES =
Size of the macOS RAM disk. A few MB is plenty for a Secret manifest; the buffer holds one small YAML document, never bulk data.
8 * 1024 * 1024
- SECTOR_BYTES =
Bytes per disk sector, used to convert RAM_DISK_BYTES into the sector count ‘hdiutil attach ram://<sectors>` expects.
512- TRAPPED_SIGNALS =
Signals whose default action would terminate the process before ‘ensure` blocks normally run; we trap them so teardown still fires.
%w[INT TERM].freeze
Class Attribute Summary collapse
-
.registry ⇒ Object
readonly
Returns the value of attribute registry.
-
.registry_mutex ⇒ Object
readonly
Returns the value of attribute registry_mutex.
Class Method Summary collapse
-
.register(workspace) ⇒ void
Register a workspace in the process-wide safety net and lazily install the ‘at_exit` hook and signal traps on first use.
-
.unregister(workspace) ⇒ void
Drop a workspace from the safety net once it has torn itself down.
-
.with(basename: "rkseal") {|path| ... } ⇒ Object
Provision a RAM-backed scratch file, yield its path, and guarantee teardown when the block returns or raises.
Instance Method Summary collapse
-
#initialize(basename: "rkseal") ⇒ SecureWorkspace
constructor
A new instance of SecureWorkspace.
-
#provision ⇒ String
Provision the RAM-backed medium and return the usable scratch path.
-
#teardown ⇒ void
Best-effort shred + unlink the scratch file and detach/teardown any RAM disk.
Constructor Details
#initialize(basename: "rkseal") ⇒ SecureWorkspace
Returns a new instance of SecureWorkspace.
145 146 147 148 149 |
# File 'lib/rkseal/secure_workspace.rb', line 145 def initialize(basename: "rkseal") @basename = sanitize_basename(basename) @medium = build_medium @path = nil end |
Class Attribute Details
.registry ⇒ Object (readonly)
Returns the value of attribute registry.
61 62 63 |
# File 'lib/rkseal/secure_workspace.rb', line 61 def registry @registry end |
.registry_mutex ⇒ Object (readonly)
Returns the value of attribute registry_mutex.
61 62 63 |
# File 'lib/rkseal/secure_workspace.rb', line 61 def registry_mutex @registry_mutex end |
Class Method Details
.register(workspace) ⇒ void
This method returns an undefined value.
Register a workspace in the process-wide safety net and lazily install the ‘at_exit` hook and signal traps on first use.
85 86 87 88 89 90 |
# File 'lib/rkseal/secure_workspace.rb', line 85 def register(workspace) @registry_mutex.synchronize do install_safety_net unless @safety_net_installed @registry << workspace unless @registry.include?(workspace) end end |
.unregister(workspace) ⇒ void
This method returns an undefined value.
Drop a workspace from the safety net once it has torn itself down.
96 97 98 |
# File 'lib/rkseal/secure_workspace.rb', line 96 def unregister(workspace) @registry_mutex.synchronize { @registry.delete(workspace) } end |
.with(basename: "rkseal") {|path| ... } ⇒ Object
Provision a RAM-backed scratch file, yield its path, and guarantee teardown when the block returns or raises.
72 73 74 75 76 77 78 |
# File 'lib/rkseal/secure_workspace.rb', line 72 def with(basename: "rkseal") workspace = new(basename: basename) path = workspace.provision yield path ensure workspace&.teardown end |
Instance Method Details
#provision ⇒ String
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
# File 'lib/rkseal/secure_workspace.rb', line 157 def provision return @path if @path # Register in the crash-safety net BEFORE touching any RAM medium, so a # SIGINT/SIGTERM landing mid-provision -- e.g. after `hdiutil attach` # succeeds but during `newfs_hfs`/`mount` -- still sweeps this workspace # and detaches the attached device. The sweep would otherwise miss a # half-provisioned workspace and leak an orphaned RAM device. Teardown is # idempotent and tolerates any partial state, so early registration is # safe and `unregister` on the rescue/teardown path keeps it accurate. self.class.register(self) directory = @medium.provision @path = File.join(directory, "#{@basename}-#{SecureRandom.hex(8)}") create_scratch_file(@path) @path rescue WorkspaceError teardown raise rescue StandardError => e teardown raise WorkspaceError, "failed to provision RAM-backed workspace: #{e.}" end |
#teardown ⇒ void
This method returns an undefined value.
Best-effort shred + unlink the scratch file and detach/teardown any RAM disk. Idempotent and must not raise on a partially-provisioned workspace (it runs from ‘ensure`/signal paths). Logs nothing sensitive.
186 187 188 189 190 191 192 193 194 195 196 197 |
# File 'lib/rkseal/secure_workspace.rb', line 186 def teardown shred_and_unlink(@path) @path = nil @medium.teardown self.class.unregister(self) nil rescue StandardError # Teardown is a safety net and must never raise. We have already nulled # @path so a retry is harmless; the RAM medium's own teardown retries a # transiently-busy detach internally. nil end |