certbot-oci-certs 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1 @@
1
+ """Certbot plug-in for Oracle Cloud Infrastructure (OCI) Certs Management Service."""
@@ -0,0 +1,313 @@
1
+ """Certbot Installer for OCI Certificates service."""
2
+ import logging
3
+ from distutils.command.config import config
4
+ from itertools import chain
5
+
6
+ from certbot import errors
7
+ from certbot import interfaces
8
+ from certbot.plugins import common
9
+
10
+ import oci
11
+
12
+ from certbot.errors import PluginError, Error
13
+ from pycparser.ply.yacc import resultlimit
14
+
15
+ #import oci.certificates_management.CertificatesManagementClient
16
+
17
+ logger = logging.getLogger(__name__)
18
+ # import sys
19
+ # logging.getLogger().addHandler(logging.StreamHandler(sys.stdout))
20
+
21
+ class OCIInstaller(common.Plugin, interfaces.Installer):
22
+ description = "OCI Certificates Service installer. Uploads the acquired certificate into the OCI Certificates Service - either as a new certificate or as a new certificate version."
23
+ certificate_main_domain = None
24
+ certificate_name = None
25
+ certificate_id = None
26
+ compartment_id = None
27
+
28
+ # I will wind up refactoring this later but for now I'm copy/pasting this from the DNS plug-in. And it uses self.credentials". So so do I here
29
+ credentials = None
30
+
31
+ # later on I won't save the credentials / config and instead just hang on to the client handle
32
+ certificates_client = None
33
+ certificates_management_client = None
34
+
35
+ @classmethod
36
+ def add_parser_arguments(cls, add):
37
+ add("compartment-id",help="Compartment OCID")
38
+ add("certificate-id",help="Certificate OCID")
39
+ add("certificate-name",help="Certificate Name")
40
+
41
+ add('auth-mode', help='Authentication mode - one of "configfile", "instance", "cloudshell"',
42
+ **{
43
+ "default": "configfile",
44
+ "choices": ['configfile', 'instance', 'cloudshell']
45
+ }
46
+ )
47
+
48
+ add('configfile', help="OCI CLI Configuration file (for authmode=configfile).")
49
+ add('profile', help="OCI configuration profile (in OCI configuration file)",**{"default":"DEFAULT"})
50
+
51
+
52
+ def __init__(self, *args, **kwargs):
53
+ super(OCIInstaller, self).__init__(*args, **kwargs)
54
+
55
+ # then initialize the SDK
56
+ match self.conf('auth-mode'):
57
+ case "configfile":
58
+ self.credentials = {
59
+ "config": oci.config.from_file(profile_name=self.conf('profile')),
60
+ "signer": None,
61
+ }
62
+ case "instance":
63
+ self.credentials = {
64
+ "config": None,
65
+ "signer": oci.auth.signers.InstancePrincipalsSecurityTokenSigner(),
66
+ }
67
+
68
+ case "cloudshell":
69
+ import os
70
+ self.credentials = {
71
+ "config": oci.config.from_file(
72
+ file_location=os.getenv("OCI_CLI_CONFIG_FILE"),
73
+ profile_name="DEFAULT"
74
+ # I am fairly certain that DEFAULT is always the right profile for cloud shell
75
+ # if not then this is the relevant code
76
+ # profile_name=self.conf('profile')
77
+ ),
78
+ # "signer": oci.auth.signers.SecurityTokenSigner(),
79
+
80
+ }
81
+
82
+ # i may or may not need this
83
+ identity_client = None
84
+
85
+ if self.credentials["signer"]:
86
+ signer = self.credentials["signer"]
87
+ identity_client = oci.identity.IdentityClient(self.credentials["config"], signer=signer)
88
+ self.certificates_client = oci.certificates.CertificatesClient(self.credentials["config"], signer=signer)
89
+ self.certificates_management_client = oci.certificates_management.CertificatesManagementClient(self.credentials["config"], signer=signer)
90
+ else:
91
+ identity_client = oci.identity.IdentityClient(self.credentials["config"])
92
+ self.certificates_management_client = oci.certificates_management.CertificatesManagementClient(self.credentials["config"])
93
+
94
+ # since we bothered to create an identity client let's use it to check that we can even talk to OCI control plane
95
+ # compartments are not subject to access control so just ask for the tenancy compartment
96
+ try:
97
+ identity_client.get_compartment(self.credentials["config"]["tenancy"])
98
+ except Exception as e:
99
+ logger.error("Failed to retrieve tenancy information. Check your configuration")
100
+ logger(e)
101
+ raise errors.PluginError(Exception("OCI configuration not valid"))
102
+
103
+ # copy these for convenience
104
+ compartment_id = self.conf("compartment-id")
105
+ certificate_id = self.conf("certificate-id")
106
+ certificate_name = self.conf("certificate-name")
107
+
108
+
109
+ # before we try to do anything we need to figure out the current situation vis a vis the cert in certs service
110
+ # possible cases:
111
+ # 1. they provided the certificate OCID
112
+ # 2. they provided the compartment OCID but no name for the certificate
113
+ # 3. they provided the compartment OCID and a name for the certificate
114
+
115
+ # Case by case
116
+ # 1. they provided the certificate OCID
117
+ if certificate_id:
118
+ logger.info("Certificate ID {} provided".format(certificate_id))
119
+
120
+ # if they provide a certificate ID then the other fields should be ignored
121
+ # using assert here will wind up raising a fatal exception
122
+ # TODO: decide if I want to do that
123
+ assert compartment_id == None
124
+ assert certificate_name == None
125
+
126
+ # NOTE: this means that we need certbot to run with permission to get the existing certificate.
127
+ # This seems OK to me at this point. But I am willing to be convinced otherwise.
128
+ # If you are reading this and have an opinion you know how to find me.
129
+ logger.debug("Looking for existing certificate with OCID {}".format(certificate_id))
130
+ response = self.certificates_management_client.get_certificate(certificate_id)
131
+ logger.debug("Returned from getting certificate.")
132
+
133
+ if len(response.data.items) != 1:
134
+ import json
135
+ logger.error("Failed to locate certificate. Response data: {}".format(json.dumps(oci.util.to_dict(response.data))))
136
+ raise PluginError(Exception("Failure attempting to locate certificate with specified OCID {}".format(certificate_id)))
137
+
138
+ # NOTE: we don't set self.compartment_id or self.certificate_name because we're ***NOT***
139
+ # going to move the existing certificate or change its name.
140
+ self.certificate_id = certificate_id
141
+
142
+ # 2. they provided the compartment OCID (but no name for the certificate)
143
+ # in which case we're going to make one up
144
+ # 3. they provided the compartment OCID and a name for the certificate
145
+ # in which case we have to go find the existing cert with that name.
146
+ # Q: should we scold them and tell them to use the OCID because that's more performant?
147
+ if compartment_id:
148
+ self.compartment_id = compartment_id
149
+ if not certificate_name:
150
+ # NOTE: I was going to let this fall through and list certificates in the compartment.
151
+ # But there exists a possibility, however remote, that someone might allow certbot to INSTALL
152
+ # a certificate, but not list existing ones in the compartment.
153
+ logger.debug("NO certificate name provided as argument. One will be automatically generated for you.")
154
+
155
+
156
+ else:
157
+ logger.info("Certificate name '{}' provided".format(certificate_name))
158
+
159
+ try:
160
+ # Try to find a certificate with that name
161
+ # NOTE: we may ot may find one with that name. A response with **EITHER** 0 or 1 items is A-OK
162
+ logger.info("Listing certificates in compartment {} with name '{}'".format(compartment_id,certificate_name))
163
+ response = self.certificates_management_client.list_certificates(compartment_id=compartment_id, name=certificate_name)
164
+ logger.debug("Returned from listing certificates")
165
+
166
+ # there had better be either zero or only one!
167
+ if len(response.data.items) == 0:
168
+ logger.info("Did not find certificate with name '{}'. The certificate will be uploaded as a new certificate to compartment {} with that name".format(certificate_name,compartment_id))
169
+
170
+ elif len(response.data.items) == 1:
171
+ logger.debug("Getting certificate OCID from response data")
172
+ self.certificate_id = response.data.items[0].id
173
+ logger.info("Existing certificate with name {} in compartment {} found. Certificate OCID is {}".format(certificate_name, compartment_id, certificate_id))
174
+
175
+ else:
176
+ # This should NOT happen because certificate names (OCI names, not domain name or whatever) are unique within a single compartment!
177
+ # But good programmers look both ways before crossing a one way street.
178
+ # So...
179
+ # also I could just do this with an assert. I should probably start doing that.
180
+ import json
181
+ raise PluginError(Exception("Expected one matching certificate but response data contained {}: {}".format(len(response.data.items), json.dumps(oci.util.to_dict(response.data)))))
182
+
183
+ except Exception as e:
184
+ logger.error("Exception occurred attempting to list existing certificates in compartment {}".format(compartment_id))
185
+ raise PluginError(Exception("Exception encountered trying to locate certificate with name {} in compartment {}".format(certificate_name,compartment_id)))
186
+
187
+
188
+ else:
189
+ raise PluginError(Error("At a minimum you must provided either (A) the OCID for an existing certificate (via --oci-certificate-id) or (B) the OCID of a compartment"))
190
+
191
+ logger.info("Plugin initialized")
192
+
193
+ def prepare(self):
194
+ pass
195
+
196
+ def more_info(self):
197
+ return "This plug-in installs the certificate into OCI Certificates Service"
198
+
199
+ def get_all_names(self):
200
+ # from the docs: "Returns all names that may be authenticated."
201
+ # what does that mean?
202
+ # ¯\_(ツ)_/¯
203
+ return []
204
+
205
+ def deploy_cert(self, domain, cert_path, key_path, chain_path, fullchain_path):
206
+ """
207
+ Actually upload Certificate to OCI Certificates service
208
+ """
209
+
210
+ def readFile(file):
211
+ with open(file) as f:
212
+ return (f.read())
213
+
214
+ # this code is cribbed from the CLI
215
+ # TODO: change this to use UpdateCertificateByImportingConfigDetails
216
+ # i.e.:
217
+ # from oci.certificates_management.models import UpdateCertificateByImportingConfigDetails
218
+
219
+ _details = {}
220
+ _details['certificateConfig'] = {}
221
+ _details['certificateConfig']['configType'] = 'IMPORTED'
222
+ _details['certificateConfig']['certChainPem'] = readFile(chain_path)
223
+ _details['certificateConfig']['privateKeyPem'] = readFile(key_path)
224
+ _details['certificateConfig']['certificatePem'] = readFile(cert_path)
225
+
226
+ response = None
227
+
228
+ if self.certificate_id:
229
+ logger.info("Preparing certificate as new version of existing certificate with OCID {}".format(self.certificate_id))
230
+
231
+ response = self.certificates_management_client.update_certificate(
232
+ certificate_id=self.certificate_id,
233
+ update_certificate_details=_details,
234
+ **{}
235
+ )
236
+
237
+ logger.debug("Back from update call")
238
+
239
+ else:
240
+ # if there's no name then make one
241
+ # NOTE: we waited until now (instead of doing it during the init) because you don't have the cert name then
242
+
243
+ # the domain here is /probably/ a good name for the certificate in OCI Certs.
244
+
245
+ # TODO: think about whether we should do another search to see if a cert with that name exists
246
+ # Reasons to do that: that's probably what they want
247
+ # Reasons to not: if that's what they wanted they would have told us that
248
+
249
+ # this code tacks on a timestamp. I decided NOT to do that
250
+ # I'm leaving the code here but commented out:
251
+ # (1) as a reminder that I changed my mind about this
252
+ # (2) as a warning to others that I changed my mind once about this already
253
+ # (3) in case I change my mind again
254
+ # from datetime import datetime
255
+ # name = "certbot-imported-cert-" + domain + datetime.now().strftime("-%Y%m%dT%H%M%S")
256
+
257
+ # name = "certbot-imported-cert-" + domain
258
+
259
+ name = domain
260
+ logger.debug("Automatically generated certificate name '{}'.".format(name))
261
+
262
+ _details['name'] = name
263
+ _details['compartmentId'] = self.compartment_id
264
+ _details['description'] = "Certificate created via import using the certbot OCI Certificate Installer plugin"
265
+
266
+ response = self.certificates_management_client.create_certificate(
267
+ create_certificate_details=_details,
268
+ **{}
269
+ )
270
+ logger.debug("Back from create certificate call")
271
+
272
+ import json
273
+ logger.debug("Response data: {}".format(json.dumps(oci.util.to_dict(response.data))))
274
+
275
+ def enhance(self, domain, enhancement, options=None): # pylint: disable=missing-docstring,no-self-use
276
+ pass # pragma: no cover
277
+
278
+ def supported_enhancements(self): # pylint: disable=missing-docstring,no-self-use
279
+ return [] # pragma: no cover
280
+
281
+ def get_all_certs_keys(self): # pylint: disable=missing-docstring,no-self-use
282
+ pass # pragma: no cover
283
+
284
+
285
+ def save(self, title=None, temporary=False):
286
+ pass
287
+
288
+ def rollback_checkpoints(self, rollback=1):
289
+ pass
290
+
291
+ def recovery_routine(self):
292
+ pass
293
+
294
+ def view_config_changes(self):
295
+ pass
296
+
297
+ def config_test(self):
298
+ pass
299
+
300
+ def restart(self):
301
+ pass
302
+
303
+ def renew_deploy(self, lineage, *args, **kwargs): # pylint: disable=missing-docstring,no-self-use
304
+ """
305
+ Renew certificates when calling `certbot renew`
306
+ """
307
+
308
+ # Run deploy_cert with the lineage params
309
+ self.deploy_cert(lineage.names()[0], lineage.cert_path, lineage.key_path, lineage.chain_path, lineage.fullchain_path)
310
+
311
+ return
312
+
313
+ interfaces.RenewDeployer.register(OCIInstaller)
@@ -0,0 +1,186 @@
1
+ Metadata-Version: 2.4
2
+ Name: certbot_oci_certs
3
+ Version: 0.1.0
4
+ Summary: OCI Certs Management Service plugin for Certbot
5
+ Home-page: https://github.com/therealcmj/certbot-oci-certs
6
+ Author: Chris Johnson
7
+ Author-email: christopher.johnson@oracle.com
8
+ License: Apache License 2.0
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Plugins
11
+ Classifier: Intended Audience :: System Administrators
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Operating System :: POSIX :: Linux
14
+ Classifier: Programming Language :: Python
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Topic :: Internet :: WWW/HTTP
18
+ Classifier: Topic :: Security
19
+ Classifier: Topic :: System :: Installation/Setup
20
+ Classifier: Topic :: System :: Networking
21
+ Classifier: Topic :: System :: Systems Administration
22
+ Classifier: Topic :: Utilities
23
+ Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*
24
+ Description-Content-Type: text/x-rst
25
+ License-File: LICENSE.txt
26
+ Requires-Dist: acme>=1.7.0
27
+ Requires-Dist: certbot>=1.7.0
28
+ Requires-Dist: setuptools
29
+ Requires-Dist: mock
30
+ Requires-Dist: oci
31
+ Dynamic: author
32
+ Dynamic: author-email
33
+ Dynamic: classifier
34
+ Dynamic: description
35
+ Dynamic: description-content-type
36
+ Dynamic: home-page
37
+ Dynamic: license
38
+ Dynamic: license-file
39
+ Dynamic: requires-dist
40
+ Dynamic: requires-python
41
+ Dynamic: summary
42
+
43
+ certbot-oci-certs
44
+ =================
45
+
46
+ Oracle Cloud Infrastructure (OCI) Installer plugin for Certbot.
47
+
48
+ This plugin automates the process of installing a certificate acquired by certbot
49
+ into OCI Certificates Management Service.
50
+
51
+ For more information on the OCI Certificates service pleae see the official documentation at
52
+ https://docs.oracle.com/en-us/iaas/Content/certificates/home.htm
53
+
54
+ Configuration:
55
+ --------------
56
+
57
+ Install and configure the OCI CLI. See https://docs.cloud.oracle.com/en-us/iaas/Content/API/SDKDocs/cliinstall.htm
58
+ for details.
59
+
60
+ To use this installer you will need:
61
+
62
+ * an OCI account with adequate permission to Create / Update / Delete certificates stored in the Certificates Management Service
63
+
64
+ Installation
65
+ ------------
66
+
67
+ I haven't published this in PyPI yet. So for the time being you need to install from source.
68
+
69
+ ::
70
+
71
+ git clone git@github.com:therealcmj/certbot-oci-certs.git
72
+ cd certbot-oci-certs
73
+ pip install .
74
+
75
+
76
+ or
77
+
78
+ ::
79
+
80
+ git clone git@github.com:therealcmj/certbot-oci-certs.git
81
+ pip install ./certbot-oci-certs
82
+
83
+
84
+ Development
85
+ -----------
86
+
87
+ If you want to work on the code you should create a virtual environment and install it there:
88
+
89
+ ::
90
+
91
+ git clone git@github.com:therealcmj/certbot-oci-certs.git
92
+ cd certbot-oci-certs
93
+ virtualenv dev
94
+ . ./dev/bin/activate
95
+ pip install -e .
96
+
97
+ You can then use your IDE as normal on the live code.
98
+
99
+ To use the debugger be sure to choose the correct virtual environment. For PyCharm go to Debug, Edit Configurations
100
+ and then update the Interpreter to point to the newly created Virtual Environment.
101
+
102
+ Arguments
103
+ ---------
104
+
105
+ As of this writing this plug-in supports the following arguments on certbot's command line:
106
+
107
+ ::
108
+
109
+ --oci-certificate-id OCI_CERTIFICATE_ID
110
+ Certificate OCID (default: None)
111
+ --oci-certificate-name OCI_CERTIFICATE_NAME
112
+ Certificate Name (default: None)
113
+ --oci-compartment-id OCI_COMPARTMENT_ID
114
+ Compartment OCID (default: None)
115
+ --oci-auth-mode {configfile,instance,cloudshell}
116
+ Authentication mode - one of "configfile", "instance", "cloudshell" (default: configfile)
117
+ --oci-configfile OCI_CONFIGFILE
118
+ OCI CLI Configuration file (for authmode=configfile). (default: None)
119
+ --oci-profile OCI_PROFILE
120
+ OCI configuration profile (in OCI configuration file) (default: DEFAULT)
121
+
122
+
123
+ You can always get a list of the available arguments by running
124
+
125
+ ::
126
+
127
+ certbot installer -h oci
128
+
129
+ Examples
130
+ --------
131
+
132
+ Assuming you have previously acquired a certificate for demosite.ociateam.com
133
+ (perhaps using the certbot-dns-oci plug-in)
134
+ you can install it via:
135
+
136
+
137
+ ::
138
+
139
+ certbot install \
140
+ --logs-dir logs --work-dir work --config-dir config \
141
+ --installer oci \
142
+ --oci-compartment $MYOCICOMPARTMENT \
143
+ --cert-path demosite.ociateam.com/cert.pem \
144
+ --key-path demosite.ociateam.com/privkey.pem \
145
+ --chain-path demosite.ociateam.com/chain.pem \
146
+ -d demosite.ociateam.com
147
+
148
+
149
+
150
+ If you want to acquire a certificate AND install it in one go using both of my plug-ins you can do that too...
151
+
152
+ ::
153
+
154
+ CERTNAME=demo$$.ociateam.com ; \
155
+ certbot run \
156
+ --test-cert \
157
+ --logs-dir logs --work-dir work --config-dir config \
158
+ --authenticator dns-oci \
159
+ --installer oci \
160
+ --oci-compartment $MYOCICOMPARTMENT \
161
+ --oci-certificate-name $CERTNAME \
162
+ --debug \
163
+ -d $CERTNAME
164
+
165
+
166
+ And to renew (just that one certificate) later it's just:
167
+
168
+ ::
169
+
170
+ CERTNAME=demo$$.ociateam.com ; \
171
+ certbot renew \
172
+ --test-cert \
173
+ --logs-dir logs --work-dir work --config-dir config \
174
+ --debug \
175
+ --cert-name $CERTNAME
176
+
177
+
178
+ CAUTION:
179
+ --------
180
+
181
+ Please do remember tat "certbot renew" tries to renew all certs nearing expiration. If you use the
182
+ --oci-certificate-name command line argument when running "certbot renew" you're going to make a mess of things.
183
+ So be cautious and renew certs one by one OR remember to leave that command line argument off!
184
+
185
+ YOU HAVE BEEN WARNED.
186
+
@@ -0,0 +1,8 @@
1
+ certbot_oci_certs/__init__.py,sha256=4I8n0MnRbQlnqCMmSJLAoN8OCipmTRk0KS7RqGUbVmY,86
2
+ certbot_oci_certs/installer.py,sha256=QeSW4Br746T2v8CJ5PY_gWg0U_czJFNoP0Qju3xYVRE,14601
3
+ certbot_oci_certs-0.1.0.dist-info/licenses/LICENSE.txt,sha256=1KQsyLaCuLy7_mruVDbako7EHDMf2ykPJLA3a1_xM8g,1867
4
+ certbot_oci_certs-0.1.0.dist-info/METADATA,sha256=cyDWnEFkMy2R9Oi1JBFgEuIYW9WH86OAo6tLWHHOyjw,5428
5
+ certbot_oci_certs-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
+ certbot_oci_certs-0.1.0.dist-info/entry_points.txt,sha256=PT3jR9PsuzhraJQdy630GQ8NJsJAZRLZYluGtPVGEnI,65
7
+ certbot_oci_certs-0.1.0.dist-info/top_level.txt,sha256=oks_XOIzKufM11BNZ3h6v43736th6im0aMB2ECH2unQ,18
8
+ certbot_oci_certs-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [certbot.plugins]
2
+ oci = certbot_oci_certs.installer:OCIInstaller
@@ -0,0 +1,35 @@
1
+ Copyright (c) 2018, 2024 Oracle and/or its affiliates. All rights reserved.
2
+
3
+ The Universal Permissive License (UPL), Version 1.0
4
+
5
+ Subject to the condition set forth below, permission is hereby granted to any
6
+ person obtaining a copy of this software, associated documentation and/or data
7
+ (collectively the "Software"), free of charge and under any and all copyright
8
+ rights in the Software, and any and all patent rights owned or freely
9
+ licensable by each licensor hereunder covering either (i) the unmodified
10
+ Software as contributed to or provided by such licensor, or (ii) the Larger
11
+ Works (as defined below), to deal in both
12
+
13
+ (a) the Software, and
14
+ (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
15
+ one is included with the Software (each a "Larger Work" to which the Software
16
+ is contributed by such licensors),
17
+
18
+ without restriction, including without limitation the rights to copy, create
19
+ derivative works of, display, perform, and distribute the Software and make,
20
+ use, sell, offer for sale, import, export, have made, and have sold the
21
+ Software and the Larger Work(s), and to sublicense the foregoing rights on
22
+ either these or other terms.
23
+
24
+ This license is subject to the following condition:
25
+ The above copyright notice and either this complete permission notice or at
26
+ a minimum a reference to the UPL must be included in all copies or
27
+ substantial portions of the Software.
28
+
29
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
31
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
32
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
33
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
34
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
35
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ certbot_oci_certs