ironclad 0.4.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6819fa2b385f31df19c7479d5ed5a0910ec213cc2c45c6ebc295808f66587a19
4
- data.tar.gz: 99037f4d626d7801aaa355ef8f4d6f528477e50e4790c4c5128242acfe8af5c9
3
+ metadata.gz: 3a87bf5f6911710634655b6619b79f20feec36c8c91033e4174b84f6d51eb643
4
+ data.tar.gz: a594caae3ea3825b7fbd5984301ad90298b163174e7ec1378274c117e1c7c05e
5
5
  SHA512:
6
- metadata.gz: a80bfbba051d04f6723269272610643d29c13dfeaeea811105a4083eea24cbd99bd8d0fb4486610b1614b4d0f38c9b9f640c8d9463df6974e7f0edb72fde5ffe
7
- data.tar.gz: d5a34169bc5fe30a3866427ea8afdcba6691c0ae3996bf199cea3aeeb43c0b85bf3b659d82e44ca9467c7e77711beb8541b190717bae9131f4a487b6fe75ce61
6
+ metadata.gz: 2e554621354d643f0ca215b07194774d2e634ad3a6d0730029ad492f10545de281c755535b74e944f44a0926727deeb9ea392dc9618acc93404bba1af74cc4b5
7
+ data.tar.gz: 79fb39bfd14bcd75f312193f60484cf970ea68fea9a1ce420302955f5062a671a99c676aa2b5c9b33994a0b0818b8b5a78dd37ed1cd136340d84e1007b058933
data/README.md CHANGED
@@ -25,8 +25,9 @@ ships:
25
25
  2. On a miss, read it from 1Password (`op read`) and seed the cache.
26
26
 
27
27
  The cache never expires. After rotating a key, refresh it with
28
- `bin/ironclad <env> --refresh`. Platforms with no supported keystore simply
29
- fetch from 1Password every call.
28
+ `bin/ironclad refresh <env>`, or refresh every environment at once with
29
+ `bin/ironclad refresh --all`. Platforms with no supported keystore simply fetch
30
+ from 1Password every call.
30
31
 
31
32
  ## Requirements
32
33
 
@@ -81,10 +82,12 @@ keys:
81
82
  ### CLI
82
83
 
83
84
  ```sh
84
- bin/ironclad # print the development key
85
- bin/ironclad production # print the production key
86
- bin/ironclad production --refresh # bypass the cache after a rotation
87
- bin/ironclad edit staging # edit staging credentials in your editor
85
+ bin/ironclad # print the development key
86
+ bin/ironclad production # print the production key
87
+ bin/ironclad refresh # re-cache the development key after a rotation
88
+ bin/ironclad refresh production # re-cache production's key
89
+ bin/ironclad refresh --all # re-cache every configured environment's key
90
+ bin/ironclad edit staging # edit staging credentials in your editor
88
91
  ```
89
92
 
90
93
  ### Capistrano
@@ -42,6 +42,7 @@ module Ironclad
42
42
 
43
43
  Print a key: bin/ironclad [env]
44
44
  Edit credentials: bin/ironclad edit [env]
45
+ After a rotation: bin/ironclad refresh [env]
45
46
  MESSAGE
46
47
  end
47
48
 
@@ -22,11 +22,13 @@ module Ironclad
22
22
  def write(name, key)
23
23
  # The key passes via argv (briefly visible to the same user's `ps`); the
24
24
  # security tool has no stdin input mode. Acceptable for a local cache.
25
+ # system returns nil when `security` cannot be executed at all, which is
26
+ # a failure to cache just as much as a non-zero exit is.
25
27
  system(
26
28
  'security', 'add-generic-password', '-U',
27
29
  '-a', @account, '-s', name, '-w', key,
28
30
  out: File::NULL, err: File::NULL
29
- )
31
+ ) || false
30
32
  end
31
33
  end
32
34
  end
@@ -15,15 +15,46 @@ module Ironclad
15
15
  end
16
16
 
17
17
  def read(name)
18
- id, status = Open3.capture2('keyctl', 'search', '@u', 'user', name)
18
+ id, _error, status = Open3.capture3(
19
+ 'keyctl', 'search', '@u', 'user', name
20
+ )
19
21
  return unless status.success?
20
22
 
21
- out, status = Open3.capture2('keyctl', 'pipe', id.chomp)
23
+ out, _error, status = Open3.capture3('keyctl', 'pipe', id.chomp)
22
24
  status.success? ? out : nil
23
25
  end
24
26
 
25
27
  def write(name, key)
26
- Open3.capture2('keyctl', 'padd', 'user', name, '@u', stdin_data: key)
28
+ # `keyctl padd` creates the key with the kernel's default permissions
29
+ # and offers no way to set them at creation, so changing them afterwards
30
+ # needs setattr, which only a possessor has. @u grants the owning user
31
+ # link permission, so linking it into @s first guarantees possession.
32
+ Open3.capture3('keyctl', 'link', '@u', '@s')
33
+
34
+ id, error, status = Open3.capture3(
35
+ 'keyctl', 'padd', 'user', name, '@u', stdin_data: key
36
+ )
37
+ return warn_failure(error, "cache #{name}") unless status.success?
38
+
39
+ # possessor 0x3f (all), user 0x2f (view, read, write, search, setattr),
40
+ # group and other none. A caller that does not possess the key gets only
41
+ # the user byte, which has to cover every operation performed here.
42
+ _out, error, status = Open3.capture3(
43
+ 'keyctl', 'setperm', id.chomp, '0x3f2f0000'
44
+ )
45
+ return warn_failure(error, "set permissions on #{name}") unless status.success?
46
+
47
+ true
48
+ end
49
+
50
+ private
51
+
52
+ def warn_failure(error, action)
53
+ detail = error.strip
54
+ message = "ironclad: could not #{action} in the Linux keyring"
55
+ message += ": #{detail}" unless detail.empty?
56
+ warn message
57
+ false
27
58
  end
28
59
  end
29
60
  end
@@ -7,7 +7,7 @@ module Ironclad
7
7
  class Null
8
8
  def read(_name) = nil
9
9
 
10
- def write(_name, _key) = nil
10
+ def write(_name, _key) = true
11
11
  end
12
12
  end
13
13
  end
@@ -6,7 +6,10 @@ require_relative 'cache/keyctl'
6
6
  require_relative 'cache/null'
7
7
 
8
8
  module Ironclad
9
- # Selects the OS keystore backend for the current platform.
9
+ # Selects the OS keystore backend for the current platform. A backend responds
10
+ # to #read(name), returning the cached string or nil, and #write(name, key),
11
+ # returning true when the key is cached and false when it is not. A backend
12
+ # with nowhere to store keys reports true: nothing to do is not a failure.
10
13
  module Cache
11
14
  module_function
12
15
 
data/lib/ironclad/cli.rb CHANGED
@@ -6,7 +6,9 @@ module Ironclad
6
6
  # Command-line entry point. Dependency-free arg parsing keeps boot cheap and
7
7
  # avoids loading Rails just to print a key.
8
8
  #
9
- # ironclad [env] [--refresh] print the credentials key (default env)
9
+ # ironclad [env] print the credentials key (default env)
10
+ # ironclad refresh [env] re-cache one environment's key
11
+ # ironclad refresh --all re-cache every environment's key
10
12
  # ironclad edit [env] edit Rails credentials for env
11
13
  # ironclad diff <file> git textconv: decrypt a credentials file
12
14
  class CLI
@@ -20,6 +22,9 @@ module Ironclad
20
22
 
21
23
  def run
22
24
  case @argv.first
25
+ when 'refresh'
26
+ @argv.shift
27
+ return refresh
23
28
  when 'edit'
24
29
  @argv.shift
25
30
  edit(@argv.shift || 'default')
@@ -40,10 +45,39 @@ module Ironclad
40
45
  private
41
46
 
42
47
  def print_key
43
- refresh = @argv.delete('--refresh') ? true : false
44
48
  env = @argv.shift || 'default'
45
49
  validate_env!(env)
46
- puts Ironclad.key(env, refresh: refresh)
50
+ puts Ironclad.key(env)
51
+ end
52
+
53
+ def refresh
54
+ return refresh_all if @argv.delete('--all')
55
+
56
+ env = @argv.shift || 'default'
57
+ validate_env!(env)
58
+ refresh_key(env) ? 0 : 1
59
+ end
60
+
61
+ def refresh_all
62
+ environments = Ironclad.config.environments
63
+ if environments.empty?
64
+ raise Error, 'No environments are configured in config/ironclad.yml.'
65
+ end
66
+
67
+ failed = environments.reject { |env| refresh_key(env) }
68
+ return 0 if failed.empty?
69
+
70
+ warn "Failed to refresh: #{failed.join(', ')}."
71
+ 1
72
+ end
73
+
74
+ def refresh_key(env)
75
+ Ironclad.key(env, refresh: true)
76
+ puts "#{env}: refreshed"
77
+ true
78
+ rescue Error => e
79
+ warn "#{env}: #{e.message}"
80
+ false
47
81
  end
48
82
 
49
83
  def edit(env)
@@ -75,12 +109,14 @@ module Ironclad
75
109
  ironclad — Rails credential keys from 1Password, cached locally.
76
110
 
77
111
  Usage:
78
- ironclad [env] [--refresh] print the credentials key (env: default)
112
+ ironclad [env] print the credentials key (env: default)
79
113
  ironclad edit [env] edit Rails credentials for env
80
114
  ironclad diff <file> git textconv: decrypt a credentials file
115
+ ironclad refresh [env] re-cache one key (env: default)
116
+ ironclad refresh --all re-cache every environment's key
81
117
  ironclad --help show this help
82
118
 
83
- --refresh re-reads from the source after a key rotation.
119
+ refresh re-reads from the source after a key rotation.
84
120
  Environments are defined in config/ironclad.yml.
85
121
  HELP
86
122
  end
@@ -22,7 +22,12 @@ module Ironclad
22
22
  end
23
23
 
24
24
  fetched = @source.read(@config.reference(environment))
25
- @cache.write(name, fetched)
25
+ cached = @cache.write(name, fetched)
26
+ if refresh && !cached
27
+ raise Error,
28
+ "Could not cache refreshed credentials key for #{environment}"
29
+ end
30
+
26
31
  fetched
27
32
  end
28
33
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ironclad
4
- VERSION = '0.4.0'
4
+ VERSION = '0.5.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ironclad
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jarrett Lusso