├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── io.go ├── io_test.go └── s3.go /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Certmagic Storage Backend for S3 2 | 3 | This library allows you to use any S3-compatible provider as key/certificate storage backend for your [Certmagic](https://github.com/caddyserver/certmagic)-enabled HTTPS server. To protect your keys from unwanted attention, client-side encryption using [secretbox](https://pkg.go.dev/golang.org/x/crypto/nacl/secretbox?tab=doc) is possible. 4 | 5 | ## What is a S3-compatible service? 6 | 7 | In the current state, any service must support the following: 8 | 9 | - v4 Signatures 10 | - HTTPS 11 | - A few basic operations: 12 | - Bucket Exists 13 | - Get Object 14 | - Put Object 15 | - Remove Object 16 | - Stat Object 17 | - List Objects 18 | 19 | Known good providers/software: 20 | 21 | - Minio (with HTTPS enabled) 22 | - Backblaze 23 | - OVH 24 | 25 | ## Credit 26 | 27 | This project was forked from [@thomersch](https://github.com/thomersch)'s wonderful [Certmagic Storage Backend for Generic S3 Providers](https://github.com/thomersch/certmagic-generic-s3) repository. 28 | 29 | ## License 30 | 31 | This project is licensed under [Apache 2.0](https://github.com/thomersch/certmagic-generic-s3/issues/1), an open source license. -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/techknowlogick/certmagic-s3 2 | 3 | go 1.22.0 4 | 5 | require ( 6 | github.com/caddyserver/caddy/v2 v2.8.4 7 | github.com/caddyserver/certmagic v0.21.3 8 | github.com/minio/minio-go/v7 v7.0.75 9 | go.uber.org/zap v1.27.0 10 | golang.org/x/crypto v0.26.0 11 | ) 12 | 13 | require ( 14 | github.com/beorn7/perks v1.0.1 // indirect 15 | github.com/caddyserver/zerossl v0.1.3 // indirect 16 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 17 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 18 | github.com/dustin/go-humanize v1.0.1 // indirect 19 | github.com/go-ini/ini v1.67.0 // indirect 20 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 21 | github.com/goccy/go-json v0.10.3 // indirect 22 | github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect 23 | github.com/google/uuid v1.6.0 // indirect 24 | github.com/klauspost/compress v1.17.9 // indirect 25 | github.com/klauspost/cpuid/v2 v2.2.8 // indirect 26 | github.com/libdns/libdns v0.2.2 // indirect 27 | github.com/mholt/acmez/v2 v2.0.2 // indirect 28 | github.com/miekg/dns v1.1.62 // indirect 29 | github.com/minio/md5-simd v1.1.2 // indirect 30 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 31 | github.com/onsi/ginkgo/v2 v2.20.1 // indirect 32 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 33 | github.com/prometheus/client_golang v1.20.2 // indirect 34 | github.com/prometheus/client_model v0.6.1 // indirect 35 | github.com/prometheus/common v0.55.0 // indirect 36 | github.com/prometheus/procfs v0.15.1 // indirect 37 | github.com/quic-go/qpack v0.4.0 // indirect 38 | github.com/quic-go/quic-go v0.46.0 // indirect 39 | github.com/rs/xid v1.6.0 // indirect 40 | github.com/zeebo/assert v1.3.0 // indirect 41 | github.com/zeebo/blake3 v0.2.4 // indirect 42 | go.uber.org/mock v0.4.0 // indirect 43 | go.uber.org/multierr v1.11.0 // indirect 44 | go.uber.org/zap/exp v0.2.0 // indirect 45 | golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 // indirect 46 | golang.org/x/mod v0.20.0 // indirect 47 | golang.org/x/net v0.28.0 // indirect 48 | golang.org/x/sync v0.8.0 // indirect 49 | golang.org/x/sys v0.24.0 // indirect 50 | golang.org/x/term v0.23.0 // indirect 51 | golang.org/x/text v0.17.0 // indirect 52 | golang.org/x/time v0.6.0 // indirect 53 | golang.org/x/tools v0.24.0 // indirect 54 | google.golang.org/protobuf v1.34.2 // indirect 55 | ) 56 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 | github.com/caddyserver/caddy/v2 v2.8.4 h1:q3pe0wpBj1OcHFZ3n/1nl4V4bxBrYoSoab7rL9BMYNk= 4 | github.com/caddyserver/caddy/v2 v2.8.4/go.mod h1:vmDAHp3d05JIvuhc24LmnxVlsZmWnUwbP5WMjzcMPWw= 5 | github.com/caddyserver/certmagic v0.21.3 h1:pqRRry3yuB4CWBVq9+cUqu+Y6E2z8TswbhNx1AZeYm0= 6 | github.com/caddyserver/certmagic v0.21.3/go.mod h1:Zq6pklO9nVRl3DIFUw9gVUfXKdpc/0qwTUAQMBlfgtI= 7 | github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA= 8 | github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= 9 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 10 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 11 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 12 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 14 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 15 | github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= 16 | github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= 17 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 18 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 19 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 20 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 21 | github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= 22 | github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 23 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 24 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 25 | github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= 26 | github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= 27 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 28 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 29 | github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= 30 | github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 31 | github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 32 | github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= 33 | github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 34 | github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s= 35 | github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= 36 | github.com/mholt/acmez/v2 v2.0.2 h1:OmK6xckte2JfKGPz4OAA8aNHTiLvGp8tLzmrd/wfSyw= 37 | github.com/mholt/acmez/v2 v2.0.2/go.mod h1:fX4c9r5jYwMyMsC+7tkYRxHibkOTgta5DIFGoe67e1U= 38 | github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= 39 | github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= 40 | github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= 41 | github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= 42 | github.com/minio/minio-go/v7 v7.0.75 h1:0uLrB6u6teY2Jt+cJUVi9cTvDRuBKWSRzSAcznRkwlE= 43 | github.com/minio/minio-go/v7 v7.0.75/go.mod h1:qydcVzV8Hqtj1VtEocfxbmVFa2siu6HGa+LDEPogjD8= 44 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 45 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 46 | github.com/onsi/ginkgo/v2 v2.20.1 h1:YlVIbqct+ZmnEph770q9Q7NVAz4wwIiVNahee6JyUzo= 47 | github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= 48 | github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= 49 | github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= 50 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 51 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 52 | github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg= 53 | github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= 54 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 55 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 56 | github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= 57 | github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= 58 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 59 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 60 | github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= 61 | github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= 62 | github.com/quic-go/quic-go v0.46.0 h1:uuwLClEEyk1DNvchH8uCByQVjo3yKL9opKulExNDs7Y= 63 | github.com/quic-go/quic-go v0.46.0/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI= 64 | github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= 65 | github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= 66 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 67 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 68 | github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= 69 | github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= 70 | github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= 71 | github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= 72 | github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= 73 | github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= 74 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 75 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 76 | go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= 77 | go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= 78 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 79 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 80 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 81 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 82 | go.uber.org/zap/exp v0.2.0 h1:FtGenNNeCATRB3CmB/yEUnjEFeJWpB/pMcy7e2bKPYs= 83 | go.uber.org/zap/exp v0.2.0/go.mod h1:t0gqAIdh1MfKv9EwN/dLwfZnJxe9ITAZN78HEWPFWDQ= 84 | golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= 85 | golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= 86 | golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA= 87 | golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= 88 | golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= 89 | golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 90 | golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= 91 | golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= 92 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 93 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 94 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 95 | golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= 96 | golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 97 | golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= 98 | golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= 99 | golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= 100 | golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 101 | golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= 102 | golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 103 | golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= 104 | golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= 105 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 106 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 107 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 108 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 109 | -------------------------------------------------------------------------------- /io.go: -------------------------------------------------------------------------------- 1 | package s3 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "errors" 7 | "io" 8 | 9 | "golang.org/x/crypto/nacl/secretbox" 10 | ) 11 | 12 | type IO interface { 13 | WrapReader(io.Reader) io.Reader 14 | ByteReader([]byte) Reader 15 | } 16 | 17 | type Reader struct { 18 | r io.Reader 19 | l int64 20 | err error 21 | } 22 | 23 | func (r Reader) Read(buf []byte) (int, error) { 24 | if r.err != nil { 25 | tr := r.err 26 | r.err = nil 27 | return 0, tr 28 | } 29 | return r.r.Read(buf) 30 | } 31 | 32 | func (r *Reader) Len() int64 { 33 | return r.l 34 | } 35 | 36 | type CleartextIO struct{} 37 | 38 | func (ci *CleartextIO) WrapReader(r io.Reader) io.Reader { 39 | return r 40 | } 41 | 42 | func (ci *CleartextIO) ByteReader(buf []byte) Reader { 43 | return Reader{bytes.NewReader(buf), int64(len(buf)), nil} 44 | } 45 | 46 | type SecretBoxIO struct { 47 | SecretKey [32]byte 48 | } 49 | 50 | func (sb *SecretBoxIO) readNonce(r io.Reader) ([24]byte, error) { 51 | var ( 52 | nonce = make([]byte, 24) 53 | n [24]byte 54 | ) 55 | l, err := r.Read(nonce) 56 | if l != 24 || err != nil { 57 | return n, nil 58 | } 59 | copy(n[:], nonce) 60 | return n, nil 61 | } 62 | 63 | func (sb *SecretBoxIO) makeNonce() ([24]byte, error) { 64 | var nonce [24]byte 65 | _, err := io.ReadFull(rand.Reader, nonce[:]) 66 | return nonce, err 67 | } 68 | 69 | func (sb *SecretBoxIO) WrapReader(r io.Reader) io.Reader { 70 | nonce, err := sb.readNonce(r) 71 | if err != nil { 72 | return Reader{nil, 0, err} 73 | } 74 | 75 | buf, _ := io.ReadAll(r) 76 | bout, ok := secretbox.Open(nil, buf, &nonce, &sb.SecretKey) 77 | if !ok { 78 | return Reader{nil, 0, errors.New("decryption failed")} 79 | } 80 | return bytes.NewReader(bout) 81 | } 82 | 83 | func (sb *SecretBoxIO) ByteReader(msg []byte) Reader { 84 | nonce, err := sb.makeNonce() 85 | out := make([]byte, len(nonce)) 86 | copy(out, nonce[:]) 87 | out = secretbox.Seal(out, msg, &nonce, &sb.SecretKey) 88 | return Reader{bytes.NewReader(out), int64(len(out)), err} 89 | } 90 | -------------------------------------------------------------------------------- /io_test.go: -------------------------------------------------------------------------------- 1 | package s3 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | ) 8 | 9 | func TestEncryptDecrypt(t *testing.T) { 10 | secret := []byte("12345678123456781234567812345678") 11 | var sbuf [32]byte 12 | 13 | copy(sbuf[:], secret) 14 | 15 | sb := SecretBoxIO{ 16 | SecretKey: sbuf, 17 | } 18 | 19 | msg := []byte("This is a very important message that shall be encrypted...") 20 | r := sb.ByteReader(msg) 21 | 22 | buf, err := io.ReadAll(r) 23 | if err != nil { 24 | t.Errorf("encrypting failed: %v", err) 25 | } 26 | 27 | w := bytes.NewReader(buf) 28 | wb := sb.WrapReader(w) 29 | 30 | buf, err = io.ReadAll(wb) 31 | if err != nil { 32 | t.Errorf("decrypting failed: %v", err) 33 | } 34 | 35 | if string(buf) != string(msg) { 36 | t.Errorf("did not decrypt, got: %s", buf) 37 | } 38 | } 39 | 40 | func TestIOWrap(t *testing.T) { 41 | empty := bytes.NewReader(nil) 42 | 43 | sb := SecretBoxIO{} 44 | wr := sb.WrapReader(empty) 45 | 46 | buf, err := io.ReadAll(wr) 47 | if err != nil { 48 | t.Errorf("reading failed: %s", err) 49 | } 50 | if len(buf) != 0 { 51 | t.Errorf("Buffer should be empty, got: %v", buf) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /s3.go: -------------------------------------------------------------------------------- 1 | package s3 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "io/fs" 10 | "strings" 11 | "time" 12 | 13 | "github.com/caddyserver/caddy/v2" 14 | "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" 15 | "github.com/caddyserver/certmagic" 16 | minio "github.com/minio/minio-go/v7" 17 | "github.com/minio/minio-go/v7/pkg/credentials" 18 | "go.uber.org/zap" 19 | ) 20 | 21 | type S3 struct { 22 | Logger *zap.Logger 23 | 24 | // S3 25 | Client *minio.Client 26 | Host string `json:"host"` 27 | Bucket string `json:"bucket"` 28 | AccessKey string `json:"access_key"` 29 | SecretKey string `json:"secret_key"` 30 | Prefix string `json:"prefix"` 31 | 32 | // EncryptionKey is optional. If you do not wish to encrypt your certficates and key inside the S3 bucket, leave it empty. 33 | EncryptionKey string `json:"encryption_key"` 34 | 35 | iowrap IO 36 | } 37 | 38 | func init() { 39 | caddy.RegisterModule(new(S3)) 40 | } 41 | 42 | func (s3 *S3) Provision(context caddy.Context) error { 43 | s3.Logger = context.Logger(s3) 44 | 45 | // S3 Client 46 | client, err := minio.New(s3.Host, &minio.Options{ 47 | Creds: credentials.NewStaticV4(s3.AccessKey, s3.SecretKey, ""), 48 | Secure: true, 49 | }) 50 | 51 | if err != nil { 52 | return err 53 | } 54 | 55 | s3.Client = client 56 | 57 | if len(s3.EncryptionKey) == 0 { 58 | s3.Logger.Info("Clear text certificate storage active") 59 | s3.iowrap = &CleartextIO{} 60 | } else if len(s3.EncryptionKey) != 32 { 61 | s3.Logger.Error("encryption key must have exactly 32 bytes") 62 | return errors.New("encryption key must have exactly 32 bytes") 63 | } else { 64 | s3.Logger.Info("Encrypted certificate storage active") 65 | sb := &SecretBoxIO{} 66 | copy(sb.SecretKey[:], []byte(s3.EncryptionKey)) 67 | s3.iowrap = sb 68 | } 69 | 70 | return nil 71 | } 72 | 73 | func (s3 *S3) CaddyModule() caddy.ModuleInfo { 74 | return caddy.ModuleInfo{ 75 | ID: "caddy.storage.s3", 76 | New: func() caddy.Module { 77 | return new(S3) 78 | }, 79 | } 80 | } 81 | 82 | var ( 83 | LockExpiration = 2 * time.Minute 84 | LockPollInterval = 1 * time.Second 85 | LockTimeout = 15 * time.Second 86 | ) 87 | 88 | func (s3 *S3) Lock(ctx context.Context, key string) error { 89 | s3.Logger.Info(fmt.Sprintf("Lock: %v", s3.objName(key))) 90 | var startedAt = time.Now() 91 | 92 | for { 93 | obj, err := s3.Client.GetObject(ctx, s3.Bucket, s3.objLockName(key), minio.GetObjectOptions{}) 94 | if err == nil { 95 | return s3.putLockFile(ctx, key) 96 | } 97 | buf, err := io.ReadAll(obj) 98 | if err != nil { 99 | // Retry 100 | continue 101 | } 102 | lt, err := time.Parse(time.RFC3339, string(buf)) 103 | if err != nil { 104 | // Lock file does not make sense, overwrite. 105 | return s3.putLockFile(ctx, key) 106 | } 107 | if lt.Add(LockTimeout).Before(time.Now()) { 108 | // Existing lock file expired, overwrite. 109 | return s3.putLockFile(ctx, key) 110 | } 111 | 112 | if startedAt.Add(LockTimeout).Before(time.Now()) { 113 | return errors.New("acquiring lock failed") 114 | } 115 | time.Sleep(LockPollInterval) 116 | } 117 | } 118 | 119 | func (s3 *S3) putLockFile(ctx context.Context, key string) error { 120 | // Object does not exist, we're creating a lock file. 121 | r := bytes.NewReader([]byte(time.Now().Format(time.RFC3339))) 122 | _, err := s3.Client.PutObject(ctx, s3.Bucket, s3.objLockName(key), r, int64(r.Len()), minio.PutObjectOptions{}) 123 | return err 124 | } 125 | 126 | func (s3 *S3) Unlock(ctx context.Context, key string) error { 127 | s3.Logger.Info(fmt.Sprintf("Release lock: %v", s3.objName(key))) 128 | return s3.Client.RemoveObject(ctx, s3.Bucket, s3.objLockName(key), minio.RemoveObjectOptions{}) 129 | } 130 | 131 | func (s3 *S3) Store(ctx context.Context, key string, value []byte) error { 132 | r := s3.iowrap.ByteReader(value) 133 | s3.Logger.Info(fmt.Sprintf("Store: %v, %v bytes", s3.objName(key), len(value))) 134 | _, err := s3.Client.PutObject(ctx, 135 | s3.Bucket, 136 | s3.objName(key), 137 | r, 138 | int64(r.Len()), 139 | minio.PutObjectOptions{}, 140 | ) 141 | return err 142 | } 143 | 144 | func (s3 *S3) Load(ctx context.Context, key string) ([]byte, error) { 145 | s3.Logger.Info(fmt.Sprintf("Load: %v", s3.objName(key))) 146 | r, err := s3.Client.GetObject(ctx, s3.Bucket, s3.objName(key), minio.GetObjectOptions{}) 147 | if err != nil { 148 | if err.Error() == "The specified key does not exist." { 149 | return nil, fs.ErrNotExist 150 | } 151 | return nil, err 152 | } else if r != nil { 153 | // AWS (at least) doesn't return an error on key doesn't exist. We have 154 | // to examine the empty object returned. 155 | _, err = r.Stat() 156 | if err != nil { 157 | er := minio.ToErrorResponse(err) 158 | if er.StatusCode == 404 { 159 | return nil, fs.ErrNotExist 160 | } 161 | } 162 | } 163 | defer r.Close() 164 | buf, err := io.ReadAll(s3.iowrap.WrapReader(r)) 165 | if err != nil { 166 | return nil, err 167 | } 168 | return buf, nil 169 | } 170 | 171 | func (s3 *S3) Delete(ctx context.Context, key string) error { 172 | s3.Logger.Info(fmt.Sprintf("Delete: %v", s3.objName(key))) 173 | return s3.Client.RemoveObject(ctx, s3.Bucket, s3.objName(key), minio.RemoveObjectOptions{}) 174 | } 175 | 176 | func (s3 *S3) Exists(ctx context.Context, key string) bool { 177 | s3.Logger.Info(fmt.Sprintf("Exists: %v", s3.objName(key))) 178 | _, err := s3.Client.StatObject(ctx, s3.Bucket, s3.objName(key), minio.StatObjectOptions{}) 179 | return err == nil 180 | } 181 | 182 | func (s3 *S3) List(ctx context.Context, prefix string, recursive bool) ([]string, error) { 183 | var keys []string 184 | for obj := range s3.Client.ListObjects(ctx, s3.Bucket, minio.ListObjectsOptions{ 185 | Prefix: s3.objName(""), 186 | Recursive: true, 187 | }) { 188 | keys = append(keys, obj.Key) 189 | } 190 | return keys, nil 191 | } 192 | 193 | func (s3 *S3) Stat(ctx context.Context, key string) (certmagic.KeyInfo, error) { 194 | s3.Logger.Info(fmt.Sprintf("Stat: %v", s3.objName(key))) 195 | var ki certmagic.KeyInfo 196 | oi, err := s3.Client.StatObject(ctx, s3.Bucket, s3.objName(key), minio.StatObjectOptions{}) 197 | if err != nil { 198 | return ki, fs.ErrNotExist 199 | } 200 | ki.Key = key 201 | ki.Size = oi.Size 202 | ki.Modified = oi.LastModified 203 | ki.IsTerminal = true 204 | return ki, nil 205 | } 206 | 207 | func (s3 *S3) objName(key string) string { 208 | return fmt.Sprintf("%s/%s", strings.TrimPrefix(s3.Prefix, "/"), strings.TrimPrefix(key, "/")) 209 | } 210 | 211 | func (s3 *S3) objLockName(key string) string { 212 | return s3.objName(key) + ".lock" 213 | } 214 | 215 | // CertMagicStorage converts s to a certmagic.Storage instance. 216 | func (s3 *S3) CertMagicStorage() (certmagic.Storage, error) { 217 | return s3, nil 218 | } 219 | 220 | func (s3 *S3) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { 221 | for d.Next() { 222 | key := d.Val() 223 | var value string 224 | 225 | if !d.Args(&value) { 226 | continue 227 | } 228 | 229 | switch key { 230 | case "host": 231 | s3.Host = value 232 | case "bucket": 233 | s3.Bucket = value 234 | case "access_key": 235 | s3.AccessKey = value 236 | case "secret_key": 237 | s3.SecretKey = value 238 | case "prefix": 239 | if value != "" { 240 | s3.Prefix = value 241 | } else { 242 | s3.Prefix = "acme" 243 | } 244 | case "encryption_key": 245 | s3.EncryptionKey = value 246 | } 247 | } 248 | return nil 249 | } 250 | 251 | var ( 252 | _ caddy.Provisioner = (*S3)(nil) 253 | _ caddy.StorageConverter = (*S3)(nil) 254 | _ caddyfile.Unmarshaler = (*S3)(nil) 255 | ) 256 | --------------------------------------------------------------------------------