cfn-guardian 0.12.0 → 0.12.1

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: 5773577700ce2126b991c8277b9d00e55938b69437bd2847783a10846bdea83d
4
- data.tar.gz: d82f6da2de87df72602901a91a93f183a17bd16155c4fd079b50864383bc790e
3
+ metadata.gz: 6a1ed7183d14b7d1bc05fdfcffc1ff4272c04ff6db43fc47e4fdcf2124b1d898
4
+ data.tar.gz: 9b3bcc2a4b2d578983f549f93a0ca482b9842a30eac3ef84fe6e4781575a5440
5
5
  SHA512:
6
- metadata.gz: '063745566801762e26b5b478dfd2ef9fe3f3c467a7253f8eae11b174816d4cf1193c7a8b152107b6e4e6348763f4d6a8017c1f3c6ad4a7a6b876c9f4c027c00e'
7
- data.tar.gz: 5d3af30d08c4eefa1ec355bf3a28f5d5306524e56ed248b56b1871cae62c18ea57cb3142109d3bfade5d12d1de79a0c39ae7f37ee7d8afd14629276407ed0e32
6
+ metadata.gz: b058796599f33efd09aff544129071b087a5cde59d87b06d3b19f5036947fc4b1679a32ba371a1a7707b05923a978c31af25e72f486b65c0f9e96d0bedff5122
7
+ data.tar.gz: 00ec6b9c0c79dd9afc146e135a0eb9512008a1dae030f8e38beed322f5ca3f6ed2ba5eaeb2a82af5b2b1335970d3f3244ff4773c65db3056d81a1b951e692e07
@@ -13,7 +13,7 @@ Resources:
13
13
  StatusCode: 200
14
14
  # enables the SSL check
15
15
  Ssl: true
16
- # boolean tp request a compressed response
16
+ # boolean to request a compressed response
17
17
  Compressed: true
18
18
  - Id: https://www.example.com
19
19
  StatusCode: 301
@@ -38,6 +38,55 @@ Resources:
38
38
  Payload: '{"name": "john"}'
39
39
  ```
40
40
 
41
+ ## HMAC signed requests
42
+
43
+ For health endpoints that require authenticated requests, Guardian can send HMAC-signed headers so your application can verify the request came from Guardian and resist replay attacks.
44
+
45
+ When enabled, the HTTP check Lambda (see [aws-lambda-http-check](https://github.com/base2/aws-lambda-http-check)) adds these headers to each request:
46
+
47
+ | Header (default prefix `X-Health`) | Description |
48
+ |-----------------------------------|-------------|
49
+ | `X-Health-Signature` | HMAC-SHA256 hex digest of the canonical string |
50
+ | `X-Health-Key-Id` | Key identifier (e.g. `default`) |
51
+ | `X-Health-Timestamp` | Unix epoch timestamp in seconds |
52
+ | `X-Health-Nonce` | Random value (e.g. UUID hex) to prevent replay |
53
+
54
+ **Configuration (Public or Internal HTTP):**
55
+
56
+ | Key | Required | Description |
57
+ |-----|----------|-------------|
58
+ | `HmacSecretSsm` | Yes | SSM Parameter Store path to the HMAC secret (SecureString). The Guardian-generated IAM role grants the Lambda `ssm:GetParameter` for this path. |
59
+ | `HmacKeyId` | No | Key id sent in the `Key-Id` header. Default: `default`. |
60
+ | `HmacHeaderPrefix` | No | Prefix for all HMAC header names. Default: `X-Health` (yields `X-Health-Signature`, `X-Health-Key-Id`, etc.). |
61
+
62
+ **Example:**
63
+
64
+ ```yaml
65
+ Resources:
66
+ Http:
67
+ - Id: https://api.example.com/health
68
+ StatusCode: 200
69
+ HmacSecretSsm: /guardian/myapp/hmac-secret
70
+ HmacKeyId: default
71
+ HmacHeaderPrefix: X-Health
72
+ ```
73
+
74
+ Internal HTTP checks support the same keys under each host:
75
+
76
+ ```yaml
77
+ Resources:
78
+ InternalHttp:
79
+ - Environment: Prod
80
+ VpcId: vpc-1234
81
+ Subnets: [subnet-abcd]
82
+ Hosts:
83
+ - Id: http://api.example.com/health
84
+ StatusCode: 200
85
+ HmacSecretSsm: /guardian/myapp/hmac-secret
86
+ ```
87
+
88
+ Create the secret in SSM (e.g. SecureString) and use the same value in your application when verifying the signature.
89
+
41
90
  ## Private HTTP Check
42
91
 
43
92
  Cloudwatch NameSpace: `InternalHttpCheck`
@@ -58,4 +107,84 @@ Resources:
58
107
  # Array of resources defining the http endpoint with the Id: key
59
108
  # All the same options as Http including ssl check on the internal endpoint
60
109
  - Id: http://api.example.com
61
- ```
110
+ ```
111
+
112
+ ## Supporting HMAC-signed health checks in your application
113
+
114
+ If you want Guardian to call a health endpoint that only accepts HMAC-authenticated requests, your server must verify the same scheme the Lambda uses.
115
+
116
+ **Canonical string (what gets signed):**
117
+
118
+ The Lambda signs this string (newline-separated, no trailing newline):
119
+
120
+ ```
121
+ METHOD\nPATH\nTIMESTAMP\nNONCE\nQUERY\nBODY_HASH
122
+ ```
123
+
124
+ - `METHOD` – HTTP method (e.g. `GET`).
125
+ - `PATH` – URL path (e.g. `/health`), no query.
126
+ - `TIMESTAMP` – Same value as the `{prefix}-Timestamp` header (Unix seconds).
127
+ - `NONCE` – Same value as the `{prefix}-Nonce` header.
128
+ - `QUERY` – Raw query string (e.g. `foo=bar` or empty).
129
+ - `BODY_HASH` – SHA-256 hex digest of the raw request body (empty string for GET; for POST/PUT, hash the body as sent).
130
+
131
+ **Verification steps (pseudo code):**
132
+
133
+ 1. Read headers (default prefix `X-Health`):
134
+ `signature`, `key_id`, `timestamp`, `nonce`.
135
+ 2. **Optional – replay protection:**
136
+ Reject if `timestamp` is too old (e.g. outside last 5 minutes).
137
+ Reject if `nonce` has been seen before (e.g. cache or DB) and treat as replay.
138
+ 3. Look up the shared secret for `key_id` (e.g. from config or secrets store – same value as in SSM for Guardian).
139
+ 4. Build the canonical string from the incoming request:
140
+ - Use request method, path, and query.
141
+ - Use `timestamp` and `nonce` from the headers.
142
+ - For body: compute SHA-256 hex of the raw request body (empty string for no body).
143
+ 5. Compute `expected = HMAC-SHA256(secret, canonical_string)` and compare to `signature` (constant-time).
144
+ 6. If equal, treat the request as authenticated.
145
+
146
+ **Pseudo code example:**
147
+
148
+ ```python
149
+ import hmac
150
+ import hashlib
151
+
152
+ def verify_health_request(request, secret_by_key_id, header_prefix="X-Health", max_age_seconds=300):
153
+ sig_h = f"{header_prefix}-Signature"
154
+ key_h = f"{header_prefix}-Key-Id"
155
+ ts_h = f"{header_prefix}-Timestamp"
156
+ nonce_h = f"{header_prefix}-Nonce"
157
+
158
+ signature = request.headers.get(sig_h)
159
+ key_id = request.headers.get(key_h)
160
+ timestamp = request.headers.get(ts_h)
161
+ nonce = request.headers.get(nonce_h)
162
+
163
+ if not all([signature, key_id, timestamp, nonce]):
164
+ return False
165
+
166
+ # Replay: reject old timestamps
167
+ if abs(int(timestamp) - time.time()) > max_age_seconds:
168
+ return False
169
+ # Replay: reject duplicate nonce (check your cache/DB)
170
+ if nonce_already_used(nonce):
171
+ return False
172
+
173
+ secret = secret_by_key_id.get(key_id)
174
+ if not secret:
175
+ return False
176
+
177
+ body_hash = hashlib.sha256(request.body or b"").hexdigest()
178
+ canonical = "\n".join([
179
+ request.method,
180
+ request.path,
181
+ timestamp,
182
+ nonce,
183
+ request.query_string or "",
184
+ body_hash,
185
+ ])
186
+ expected = hmac.new(secret.encode(), canonical.encode(), hashlib.sha256).hexdigest()
187
+ return hmac.compare_digest(expected, signature)
188
+ ```
189
+
190
+ Use the same HMAC secret in SSM (Guardian config) and in your app’s config or secrets store. Keep the secret in SecureString or equivalent and restrict access appropriately.
@@ -42,7 +42,7 @@ module CfnGuardian
42
42
  @name = 'HttpCheck'
43
43
  @package = 'http-check'
44
44
  @handler = 'handler.http_check'
45
- @version = '077c726ed691a1176caf95497b8b02f05f00e0cb'
45
+ @version = '685c17ced2429954fccbf8b8b8f2132b37b3f4ff'
46
46
  @runtime = 'python3.11'
47
47
  end
48
48
  end
@@ -55,6 +55,10 @@ module CfnGuardian
55
55
  @user_agent = resource.fetch('UserAgent',nil)
56
56
  @payload = resource.fetch('Payload',nil)
57
57
  @compressed = resource.fetch('Compressed',false)
58
+ @hmac_secret_ssm = resource.fetch('HmacSecretSsm',nil)
59
+ @hmac_key_id = resource.fetch('HmacKeyId','default')
60
+ @hmac_header_prefix = resource.fetch('HmacHeaderPrefix','X-Health')
61
+ @report_response_body = resource.fetch('ReportResponseBody',false)
58
62
  end
59
63
 
60
64
  def payload
@@ -69,8 +73,18 @@ module CfnGuardian
69
73
  payload['USER_AGENT'] = @user_agent unless @user_agent.nil?
70
74
  payload['PAYLOAD'] = @payload unless @payload.nil?
71
75
  payload['COMPRESSED'] = '1' if @compressed
76
+ payload['REPORT_RESPONSE_BODY'] = '1' if @report_response_body
77
+ unless @hmac_secret_ssm.nil?
78
+ payload['HMAC_SECRET_SSM'] = @hmac_secret_ssm
79
+ payload['HMAC_KEY_ID'] = @hmac_key_id
80
+ payload['HMAC_HEADER_PREFIX'] = @hmac_header_prefix
81
+ end
72
82
  return payload.to_json
73
83
  end
84
+
85
+ def ssm_parameters
86
+ @hmac_secret_ssm.nil? ? [] : [@hmac_secret_ssm]
87
+ end
74
88
  end
75
89
 
76
90
  class WebSocketEvent < BaseEvent
@@ -25,6 +25,7 @@ module CfnGuardian::Resource
25
25
  alarm.statistic = 'Minimum'
26
26
  alarm.threshold = 10000000000
27
27
  alarm.evaluation_periods = 1
28
+ alarm.comparison_operator = 'LessThanThreshold'
28
29
  @alarms.push(alarm)
29
30
 
30
31
  alarm = CfnGuardian::Models::DMSClusterAlarm.new(@resource)
@@ -34,6 +35,7 @@ module CfnGuardian::Resource
34
35
  alarm.threshold = 20000000000
35
36
  alarm.evaluation_periods = 1
36
37
  alarm.alarm_action = 'Warning'
38
+ alarm.comparison_operator = 'LessThanThreshold'
37
39
  @alarms.push(alarm)
38
40
  end
39
41
  end
@@ -1,4 +1,4 @@
1
1
  module CfnGuardian
2
- VERSION = "0.12.0"
2
+ VERSION = "0.12.1"
3
3
  CHANGE_SET_VERSION = VERSION.gsub('.', '-').freeze
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cfn-guardian
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.0
4
+ version: 0.12.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Guslington
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-09-16 00:00:00.000000000 Z
11
+ date: 2026-03-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor