Browse Source

feat: github provider (#4459)

Signed-off-by: Gustavo <gustavo@externalsecrets.com>
Gustavo Fernandes de Carvalho 1 year ago
parent
commit
a25421bc61

+ 52 - 0
apis/externalsecrets/v1beta1/secretstore_github_types.go

@@ -0,0 +1,52 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1beta1
+
+import (
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+// Configures a store to push secrets to Github Actions.
+type GithubProvider struct {
+	// URL configures the Github instance URL. Defaults to https://github.com/.
+	//+kubebuilder:default="https://github.com/"
+	URL string `json:"url,omitempty"`
+	// Upload URL for enterprise instances. Default to URL.
+	//+optional
+	UploadURL string `json:"uploadURL,omitempty"`
+	// auth configures how secret-manager authenticates with a Github instance.
+	Auth GithubAppAuth `json:"auth"`
+
+	// appID specifies the Github APP that will be used to authenticate the client
+	AppID int64 `json:"appID"`
+
+	// installationID specifies the Github APP installation that will be used to authenticate the client
+	InstallationID int64 `json:"installationID"`
+
+	// organization will be used to fetch secrets from the Github organization
+	Organization string `json:"organization"`
+
+	// repository will be used to fetch secrets from the Github repository within an organization
+	//+optional
+	Repository string `json:"repository,omitempty"`
+
+	// environment will be used to fetch secrets from a particular environment within a github repository
+	//+optional
+	Environment string `json:"environment,omitempty"`
+}
+
+type GithubAppAuth struct {
+	PrivateKey esmeta.SecretKeySelector `json:"privateKey"`
+}

+ 4 - 0
apis/externalsecrets/v1beta1/secretstore_types.go

@@ -105,6 +105,10 @@ type SecretStoreProvider struct {
 	// +optional
 	// +optional
 	YandexLockbox *YandexLockboxProvider `json:"yandexlockbox,omitempty"`
 	YandexLockbox *YandexLockboxProvider `json:"yandexlockbox,omitempty"`
 
 
+	// Github configures this store to push Github Action secrets using Github API provider
+	// +optional
+	Github *GithubProvider `json:"github,omitempty"`
+
 	// GitLab configures this store to sync secrets using GitLab Variables provider
 	// GitLab configures this store to sync secrets using GitLab Variables provider
 	// +optional
 	// +optional
 	Gitlab *GitlabProvider `json:"gitlab,omitempty"`
 	Gitlab *GitlabProvider `json:"gitlab,omitempty"`

+ 37 - 0
apis/externalsecrets/v1beta1/zz_generated.deepcopy.go

@@ -1746,6 +1746,38 @@ func (in *GenericStoreValidator) DeepCopy() *GenericStoreValidator {
 }
 }
 
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GithubAppAuth) DeepCopyInto(out *GithubAppAuth) {
+	*out = *in
+	in.PrivateKey.DeepCopyInto(&out.PrivateKey)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GithubAppAuth.
+func (in *GithubAppAuth) DeepCopy() *GithubAppAuth {
+	if in == nil {
+		return nil
+	}
+	out := new(GithubAppAuth)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GithubProvider) DeepCopyInto(out *GithubProvider) {
+	*out = *in
+	in.Auth.DeepCopyInto(&out.Auth)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GithubProvider.
+func (in *GithubProvider) DeepCopy() *GithubProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(GithubProvider)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *GitlabAuth) DeepCopyInto(out *GitlabAuth) {
 func (in *GitlabAuth) DeepCopyInto(out *GitlabAuth) {
 	*out = *in
 	*out = *in
 	in.SecretRef.DeepCopyInto(&out.SecretRef)
 	in.SecretRef.DeepCopyInto(&out.SecretRef)
@@ -2595,6 +2627,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
 		*out = new(YandexLockboxProvider)
 		*out = new(YandexLockboxProvider)
 		(*in).DeepCopyInto(*out)
 		(*in).DeepCopyInto(*out)
 	}
 	}
+	if in.Github != nil {
+		in, out := &in.Github, &out.Github
+		*out = new(GithubProvider)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.Gitlab != nil {
 	if in.Gitlab != nil {
 		in, out := &in.Gitlab, &out.Gitlab
 		in, out := &in.Gitlab, &out.Gitlab
 		*out = new(GitlabProvider)
 		*out = new(GitlabProvider)

+ 77 - 0
config/crds/bases/external-secrets.io_clustersecretstores.yaml

@@ -3753,6 +3753,83 @@ spec:
                         description: ProjectID project where secret is located
                         description: ProjectID project where secret is located
                         type: string
                         type: string
                     type: object
                     type: object
+                  github:
+                    description: Github configures this store to push Github Action
+                      secrets using Github API provider
+                    properties:
+                      appID:
+                        description: appID specifies the Github APP that will be used
+                          to authenticate the client
+                        format: int64
+                        type: integer
+                      auth:
+                        description: auth configures how secret-manager authenticates
+                          with a Github instance.
+                        properties:
+                          privateKey:
+                            description: |-
+                              A reference to a specific 'key' within a Secret resource.
+                              In some instances, `key` is a required field.
+                            properties:
+                              key:
+                                description: |-
+                                  A key in the referenced Secret.
+                                  Some instances of this field may be defaulted, in others it may be required.
+                                maxLength: 253
+                                minLength: 1
+                                pattern: ^[-._a-zA-Z0-9]+$
+                                type: string
+                              name:
+                                description: The name of the Secret resource being
+                                  referred to.
+                                maxLength: 253
+                                minLength: 1
+                                pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                type: string
+                              namespace:
+                                description: |-
+                                  The namespace of the Secret resource being referred to.
+                                  Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                maxLength: 63
+                                minLength: 1
+                                pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                type: string
+                            type: object
+                        required:
+                        - privateKey
+                        type: object
+                      environment:
+                        description: environment will be used to fetch secrets from
+                          a particular environment within a github repository
+                        type: string
+                      installationID:
+                        description: installationID specifies the Github APP installation
+                          that will be used to authenticate the client
+                        format: int64
+                        type: integer
+                      organization:
+                        description: organization will be used to fetch secrets from
+                          the Github organization
+                        type: string
+                      repository:
+                        description: repository will be used to fetch secrets from
+                          the Github repository within an organization
+                        type: string
+                      uploadURL:
+                        description: Upload URL for enterprise instances. Default
+                          to URL.
+                        type: string
+                      url:
+                        default: https://github.com/
+                        description: URL configures the Github instance URL. Defaults
+                          to https://github.com/.
+                        type: string
+                    required:
+                    - appID
+                    - auth
+                    - installationID
+                    - organization
+                    type: object
                   gitlab:
                   gitlab:
                     description: GitLab configures this store to sync secrets using
                     description: GitLab configures this store to sync secrets using
                       GitLab Variables provider
                       GitLab Variables provider

+ 77 - 0
config/crds/bases/external-secrets.io_secretstores.yaml

@@ -3753,6 +3753,83 @@ spec:
                         description: ProjectID project where secret is located
                         description: ProjectID project where secret is located
                         type: string
                         type: string
                     type: object
                     type: object
+                  github:
+                    description: Github configures this store to push Github Action
+                      secrets using Github API provider
+                    properties:
+                      appID:
+                        description: appID specifies the Github APP that will be used
+                          to authenticate the client
+                        format: int64
+                        type: integer
+                      auth:
+                        description: auth configures how secret-manager authenticates
+                          with a Github instance.
+                        properties:
+                          privateKey:
+                            description: |-
+                              A reference to a specific 'key' within a Secret resource.
+                              In some instances, `key` is a required field.
+                            properties:
+                              key:
+                                description: |-
+                                  A key in the referenced Secret.
+                                  Some instances of this field may be defaulted, in others it may be required.
+                                maxLength: 253
+                                minLength: 1
+                                pattern: ^[-._a-zA-Z0-9]+$
+                                type: string
+                              name:
+                                description: The name of the Secret resource being
+                                  referred to.
+                                maxLength: 253
+                                minLength: 1
+                                pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                type: string
+                              namespace:
+                                description: |-
+                                  The namespace of the Secret resource being referred to.
+                                  Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                maxLength: 63
+                                minLength: 1
+                                pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                type: string
+                            type: object
+                        required:
+                        - privateKey
+                        type: object
+                      environment:
+                        description: environment will be used to fetch secrets from
+                          a particular environment within a github repository
+                        type: string
+                      installationID:
+                        description: installationID specifies the Github APP installation
+                          that will be used to authenticate the client
+                        format: int64
+                        type: integer
+                      organization:
+                        description: organization will be used to fetch secrets from
+                          the Github organization
+                        type: string
+                      repository:
+                        description: repository will be used to fetch secrets from
+                          the Github repository within an organization
+                        type: string
+                      uploadURL:
+                        description: Upload URL for enterprise instances. Default
+                          to URL.
+                        type: string
+                      url:
+                        default: https://github.com/
+                        description: URL configures the Github instance URL. Defaults
+                          to https://github.com/.
+                        type: string
+                    required:
+                    - appID
+                    - auth
+                    - installationID
+                    - organization
+                    type: object
                   gitlab:
                   gitlab:
                     description: GitLab configures this store to sync secrets using
                     description: GitLab configures this store to sync secrets using
                       GitLab Variables provider
                       GitLab Variables provider

+ 134 - 0
deploy/crds/bundle.yaml

@@ -4267,6 +4267,73 @@ spec:
                           description: ProjectID project where secret is located
                           description: ProjectID project where secret is located
                           type: string
                           type: string
                       type: object
                       type: object
+                    github:
+                      description: Github configures this store to push Github Action secrets using Github API provider
+                      properties:
+                        appID:
+                          description: appID specifies the Github APP that will be used to authenticate the client
+                          format: int64
+                          type: integer
+                        auth:
+                          description: auth configures how secret-manager authenticates with a Github instance.
+                          properties:
+                            privateKey:
+                              description: |-
+                                A reference to a specific 'key' within a Secret resource.
+                                In some instances, `key` is a required field.
+                              properties:
+                                key:
+                                  description: |-
+                                    A key in the referenced Secret.
+                                    Some instances of this field may be defaulted, in others it may be required.
+                                  maxLength: 253
+                                  minLength: 1
+                                  pattern: ^[-._a-zA-Z0-9]+$
+                                  type: string
+                                name:
+                                  description: The name of the Secret resource being referred to.
+                                  maxLength: 253
+                                  minLength: 1
+                                  pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                  type: string
+                                namespace:
+                                  description: |-
+                                    The namespace of the Secret resource being referred to.
+                                    Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                  maxLength: 63
+                                  minLength: 1
+                                  pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                  type: string
+                              type: object
+                          required:
+                            - privateKey
+                          type: object
+                        environment:
+                          description: environment will be used to fetch secrets from a particular environment within a github repository
+                          type: string
+                        installationID:
+                          description: installationID specifies the Github APP installation that will be used to authenticate the client
+                          format: int64
+                          type: integer
+                        organization:
+                          description: organization will be used to fetch secrets from the Github organization
+                          type: string
+                        repository:
+                          description: repository will be used to fetch secrets from the Github repository within an organization
+                          type: string
+                        uploadURL:
+                          description: Upload URL for enterprise instances. Default to URL.
+                          type: string
+                        url:
+                          default: https://github.com/
+                          description: URL configures the Github instance URL. Defaults to https://github.com/.
+                          type: string
+                      required:
+                        - appID
+                        - auth
+                        - installationID
+                        - organization
+                      type: object
                     gitlab:
                     gitlab:
                       description: GitLab configures this store to sync secrets using GitLab Variables provider
                       description: GitLab configures this store to sync secrets using GitLab Variables provider
                       properties:
                       properties:
@@ -11401,6 +11468,73 @@ spec:
                           description: ProjectID project where secret is located
                           description: ProjectID project where secret is located
                           type: string
                           type: string
                       type: object
                       type: object
+                    github:
+                      description: Github configures this store to push Github Action secrets using Github API provider
+                      properties:
+                        appID:
+                          description: appID specifies the Github APP that will be used to authenticate the client
+                          format: int64
+                          type: integer
+                        auth:
+                          description: auth configures how secret-manager authenticates with a Github instance.
+                          properties:
+                            privateKey:
+                              description: |-
+                                A reference to a specific 'key' within a Secret resource.
+                                In some instances, `key` is a required field.
+                              properties:
+                                key:
+                                  description: |-
+                                    A key in the referenced Secret.
+                                    Some instances of this field may be defaulted, in others it may be required.
+                                  maxLength: 253
+                                  minLength: 1
+                                  pattern: ^[-._a-zA-Z0-9]+$
+                                  type: string
+                                name:
+                                  description: The name of the Secret resource being referred to.
+                                  maxLength: 253
+                                  minLength: 1
+                                  pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                  type: string
+                                namespace:
+                                  description: |-
+                                    The namespace of the Secret resource being referred to.
+                                    Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                  maxLength: 63
+                                  minLength: 1
+                                  pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                  type: string
+                              type: object
+                          required:
+                            - privateKey
+                          type: object
+                        environment:
+                          description: environment will be used to fetch secrets from a particular environment within a github repository
+                          type: string
+                        installationID:
+                          description: installationID specifies the Github APP installation that will be used to authenticate the client
+                          format: int64
+                          type: integer
+                        organization:
+                          description: organization will be used to fetch secrets from the Github organization
+                          type: string
+                        repository:
+                          description: repository will be used to fetch secrets from the Github repository within an organization
+                          type: string
+                        uploadURL:
+                          description: Upload URL for enterprise instances. Default to URL.
+                          type: string
+                        url:
+                          default: https://github.com/
+                          description: URL configures the Github instance URL. Defaults to https://github.com/.
+                          type: string
+                      required:
+                        - appID
+                        - auth
+                        - installationID
+                        - organization
+                      type: object
                     gitlab:
                     gitlab:
                       description: GitLab configures this store to sync secrets using GitLab Variables provider
                       description: GitLab configures this store to sync secrets using GitLab Variables provider
                       properties:
                       properties:

+ 156 - 0
docs/api/spec.md

@@ -4620,6 +4620,148 @@ or a namespaced SecretStore.</p>
 </h3>
 </h3>
 <p>
 <p>
 </p>
 </p>
+<h3 id="external-secrets.io/v1beta1.GithubAppAuth">GithubAppAuth
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.GithubProvider">GithubProvider</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>privateKey</code></br>
+<em>
+<a href="https://pkg.go.dev/github.com/external-secrets/external-secrets/apis/meta/v1#SecretKeySelector">
+External Secrets meta/v1.SecretKeySelector
+</a>
+</em>
+</td>
+<td>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1beta1.GithubProvider">GithubProvider
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.SecretStoreProvider">SecretStoreProvider</a>)
+</p>
+<p>
+<p>Configures a store to push secrets to Github Actions.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>url</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>URL configures the Github instance URL. Defaults to <a href="https://github.com/">https://github.com/</a>.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>uploadURL</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Upload URL for enterprise instances. Default to URL.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>auth</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.GithubAppAuth">
+GithubAppAuth
+</a>
+</em>
+</td>
+<td>
+<p>auth configures how secret-manager authenticates with a Github instance.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>appID</code></br>
+<em>
+int64
+</em>
+</td>
+<td>
+<p>appID specifies the Github APP that will be used to authenticate the client</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>installationID</code></br>
+<em>
+int64
+</em>
+</td>
+<td>
+<p>installationID specifies the Github APP installation that will be used to authenticate the client</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>organization</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>organization will be used to fetch secrets from the Github organization</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>repository</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>repository will be used to fetch secrets from the Github repository within an organization</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>environment</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>environment will be used to fetch secrets from a particular environment within a github repository</p>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="external-secrets.io/v1beta1.GitlabAuth">GitlabAuth
 <h3 id="external-secrets.io/v1beta1.GitlabAuth">GitlabAuth
 </h3>
 </h3>
 <p>
 <p>
@@ -6770,6 +6912,20 @@ YandexLockboxProvider
 </tr>
 </tr>
 <tr>
 <tr>
 <td>
 <td>
+<code>github</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.GithubProvider">
+GithubProvider
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Github configures this store to push Github Action secrets using Github API provider</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>gitlab</code></br>
 <code>gitlab</code></br>
 <em>
 <em>
 <a href="#external-secrets.io/v1beta1.GitlabProvider">
 <a href="#external-secrets.io/v1beta1.GitlabProvider">

+ 27 - 0
docs/provider/github.md

@@ -0,0 +1,27 @@
+## Github
+
+External Secrets Operator integrates with Github to sync Kubernetes secrets with [Github Actions secrets](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions).
+
+### Configuring Github provider
+
+The Github API requires to install the ESO app to your Github organisation in order to use the Github provider features.
+
+### Configuring the secret store
+
+Verify that `github` provider is listed in the `Kind=SecretStore`. The properties `appID`, `installationID`, `organization` are required to register the provider. In addition, authentication has to be provided.
+
+Optionally, to target `repository` and `environment` secrets, the fields `repository` and `environment` need also to be added.
+
+```yaml
+{% include 'github-secret-store.yaml' %}
+```
+
+**NOTE:** In case of a `ClusterSecretStore`, Be sure to provide `namespace` in `accessToken` with the namespace where the secret resides.
+
+### Pushing to an external secret
+
+To sync a Kubernetes secret with an external Github secret we need to create a PushSecret, this means a `Kind=PushSecret` is needed.
+
+```yaml
+{% include 'github-push-secret.yaml' %}
+```

+ 18 - 0
docs/snippets/github-push-secret.yaml

@@ -0,0 +1,18 @@
+apiVersion: external-secrets.io/v1alpha1
+kind: PushSecret
+metadata:
+  name: github-push-secret-example
+spec:
+  deletionPolicy: Delete
+  refreshInterval: 10m # Refresh interval for which push secret will reconcile
+  secretStoreRefs: # A list of secret stores to push secrets to
+    - name: github # Must match SecretStore on the cluster
+      kind: SecretStore
+  selector:
+    secret:
+      name: EXTSERCRET # Remote Github actions secret that we want to sync with the kubernetes secret
+  data:
+    - match:
+        secretKey: extsecret # Source Kubernetes secret key containing the secret
+        remoteRef:
+          remoteKey: EXTSECRET # Key of the kubernetes secret to push

+ 19 - 0
docs/snippets/github-secret-store.yaml

@@ -0,0 +1,19 @@
+apiVersion: external-secrets.io/v1beta1
+kind: SecretStore
+metadata:
+  name: github
+spec:
+  provider:
+    # provider type: github
+    github:
+      appID: "**app ID goes here**"
+      # url: Default "https://github.com/", for enterprise Github instances uncomment and add your domain like "https://github.domain.com/"
+      # uploadURL: Default "https://github.com"
+      auth:
+        privateKey:
+          name: github-app-private-key
+          key: privateKey.pem
+      installationID: "**installation ID goes here**"
+      organization: "Github **organization name goes here**"
+      #repository: "Optional. set this for repository/environment secrets"
+      #environment: "Optional. set this for environment secrets"

+ 2 - 0
go.mod

@@ -76,11 +76,13 @@ require (
 	github.com/alibabacloud-go/tea-utils/v2 v2.0.7
 	github.com/alibabacloud-go/tea-utils/v2 v2.0.7
 	github.com/aliyun/credentials-go v1.4.3
 	github.com/aliyun/credentials-go v1.4.3
 	github.com/avast/retry-go/v4 v4.6.0
 	github.com/avast/retry-go/v4 v4.6.0
+	github.com/bradleyfalzon/ghinstallation/v2 v2.8.0
 	github.com/cenkalti/backoff/v4 v4.3.0
 	github.com/cenkalti/backoff/v4 v4.3.0
 	github.com/cyberark/conjur-api-go v0.12.12
 	github.com/cyberark/conjur-api-go v0.12.12
 	github.com/fortanix/sdkms-client-go v0.4.0
 	github.com/fortanix/sdkms-client-go v0.4.0
 	github.com/go-openapi/strfmt v0.23.0
 	github.com/go-openapi/strfmt v0.23.0
 	github.com/golang-jwt/jwt/v5 v5.2.1
 	github.com/golang-jwt/jwt/v5 v5.2.1
+	github.com/google/go-github/v56 v56.0.0
 	github.com/grafana/grafana-openapi-client-go v0.0.0-20250108132429-8d7e1f158f65
 	github.com/grafana/grafana-openapi-client-go v0.0.0-20250108132429-8d7e1f158f65
 	github.com/hashicorp/golang-lru v1.0.2
 	github.com/hashicorp/golang-lru v1.0.2
 	github.com/hashicorp/vault/api/auth/aws v0.9.0
 	github.com/hashicorp/vault/api/auth/aws v0.9.0

+ 4 - 0
go.sum

@@ -211,6 +211,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=
 github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=
 github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=
 github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=
+github.com/bradleyfalzon/ghinstallation/v2 v2.8.0 h1:yUmoVv70H3J4UOqxqsee39+KlXxNEDfTbAp8c/qULKk=
+github.com/bradleyfalzon/ghinstallation/v2 v2.8.0/go.mod h1:fmPmvCiBWhJla3zDv9ZTQSZc8AbwyRnGW1yg5ep1Pcs=
 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -400,6 +402,8 @@ github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-github/v56 v56.0.0 h1:TysL7dMa/r7wsQi44BjqlwaHvwlFlqkK8CtBWCX3gb4=
+github.com/google/go-github/v56 v56.0.0/go.mod h1:D8cdcX98YWJvi7TLo7zM4/h8ZTx6u6fwGEkCdisopo0=
 github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
 github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
 github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
 github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=

+ 1 - 0
hack/api-docs/mkdocs.yml

@@ -126,6 +126,7 @@ nav:
       - Yandex Lockbox: provider/yandex-lockbox.md
       - Yandex Lockbox: provider/yandex-lockbox.md
       - Alibaba Cloud: provider/alibaba.md
       - Alibaba Cloud: provider/alibaba.md
       - GitLab Variables: provider/gitlab-variables.md
       - GitLab Variables: provider/gitlab-variables.md
+      - Github Actions Secrets: provider/github.md
       - Oracle Vault: provider/oracle-vault.md
       - Oracle Vault: provider/oracle-vault.md
       - 1Password Secrets Automation: provider/1password-automation.md
       - 1Password Secrets Automation: provider/1password-automation.md
       - Webhook: provider/webhook.md
       - Webhook: provider/webhook.md

+ 46 - 0
pkg/provider/github/auth.go

@@ -0,0 +1,46 @@
+// /*
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//	http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// */
+package github
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+
+	"github.com/bradleyfalzon/ghinstallation/v2"
+	github "github.com/google/go-github/v56/github"
+
+	"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
+)
+
+func (g *Client) AuthWithPrivateKey(ctx context.Context) (*github.Client, error) {
+	privateKey, err := resolvers.SecretKeyRef(ctx, g.crClient, g.storeKind, g.namespace, &g.provider.Auth.PrivateKey)
+	if err != nil {
+		return nil, fmt.Errorf("couldn't get private key from secret: resolvers.SecretKeyRef failed with error %w", err)
+	}
+
+	itr, err := ghinstallation.New(http.DefaultTransport, g.provider.AppID, g.provider.InstallationID, []byte(privateKey))
+	if err != nil {
+		return nil, fmt.Errorf("could not instantiate new installation transport: %w", err)
+	}
+	client := github.NewClient(&http.Client{Transport: itr})
+	if (g.provider.URL != "") && (g.provider.URL != "https://github.com/") {
+		uploadURL := g.provider.UploadURL
+		if uploadURL == "" {
+			uploadURL = g.provider.URL
+		}
+		return client.WithEnterpriseURLs(g.provider.URL, uploadURL)
+	}
+	return client, nil
+}

+ 160 - 0
pkg/provider/github/client.go

@@ -0,0 +1,160 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package github
+
+import (
+	"context"
+	crypto_rand "crypto/rand"
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"time"
+
+	github "github.com/google/go-github/v56/github"
+	"golang.org/x/crypto/nacl/box"
+	corev1 "k8s.io/api/core/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+)
+
+// https://github.com/external-secrets/external-secrets/issues/644
+var _ esv1beta1.SecretsClient = &Client{}
+
+type ActionsServiceClient interface {
+	CreateOrUpdateOrgSecret(ctx context.Context, org string, eSecret *github.EncryptedSecret) (response *github.Response, err error)
+	GetOrgSecret(ctx context.Context, org string, name string) (*github.Secret, *github.Response, error)
+	ListOrgSecrets(ctx context.Context, org string, opts *github.ListOptions) (*github.Secrets, *github.Response, error)
+}
+type Client struct {
+	crClient         client.Client
+	store            esv1beta1.GenericStore
+	provider         *esv1beta1.GithubProvider
+	baseClient       github.ActionsService
+	namespace        string
+	storeKind        string
+	repoID           int64
+	getSecretFn      func(ctx context.Context, ref esv1beta1.PushSecretRemoteRef) (*github.Secret, *github.Response, error)
+	getPublicKeyFn   func(ctx context.Context) (*github.PublicKey, *github.Response, error)
+	createOrUpdateFn func(ctx context.Context, eSecret *github.EncryptedSecret) (*github.Response, error)
+	listSecretsFn    func(ctx context.Context) (*github.Secrets, *github.Response, error)
+	deleteSecretFn   func(ctx context.Context, ref esv1beta1.PushSecretRemoteRef) (*github.Response, error)
+}
+
+func (g *Client) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushSecretRemoteRef) error {
+	_, err := g.deleteSecretFn(ctx, remoteRef)
+	if err != nil {
+		return fmt.Errorf("failed to delete secret: %w", err)
+	}
+	return nil
+}
+
+func (g *Client) SecretExists(ctx context.Context, ref esv1beta1.PushSecretRemoteRef) (bool, error) {
+	githubSecret, _, err := g.getSecretFn(ctx, ref)
+	if err != nil {
+		return false, fmt.Errorf("error fetching secret: %w", err)
+	}
+	if githubSecret != nil {
+		return true, nil
+	}
+	return false, nil
+}
+
+func (g *Client) PushSecret(ctx context.Context, secret *corev1.Secret, remoteRef esv1beta1.PushSecretData) error {
+	githubSecret, _, err := g.getSecretFn(ctx, remoteRef)
+	if err != nil {
+		return fmt.Errorf("error fetching secret: %w", err)
+	}
+
+	// If the secret already exists, we need to update it.
+	// First at all, we need the organization public key to encrypt the secret.
+	publicKey, _, err := g.getPublicKeyFn(ctx)
+	if err != nil {
+		return fmt.Errorf("error fetching public key: %w", err)
+	}
+
+	decodedPublicKey, err := base64.StdEncoding.DecodeString(publicKey.GetKey())
+	if err != nil {
+		return fmt.Errorf("unable to decode public key: %w", err)
+	}
+
+	var boxKey [32]byte
+	copy(boxKey[:], decodedPublicKey)
+	var ok bool
+	// default to full secret.
+	value, err := json.Marshal(secret.Data)
+	if err != nil {
+		return fmt.Errorf("json.Marshal failed with error %w", err)
+	}
+	// if key is specified, overwrite to key only
+	if remoteRef.GetSecretKey() != "" {
+		value, ok = secret.Data[remoteRef.GetSecretKey()]
+		if !ok {
+			return fmt.Errorf("key %s not found in secret", remoteRef.GetSecretKey())
+		}
+	}
+
+	encryptedBytes, err := box.SealAnonymous([]byte{}, value, &boxKey, crypto_rand.Reader)
+	if err != nil {
+		return fmt.Errorf("box.SealAnonymous failed with error %w", err)
+	}
+
+	encryptedString := base64.StdEncoding.EncodeToString(encryptedBytes)
+	keyID := publicKey.GetKeyID()
+	encryptedSecret := &github.EncryptedSecret{
+		Name:           githubSecret.Name,
+		KeyID:          keyID,
+		EncryptedValue: encryptedString,
+		Visibility:     githubSecret.Visibility,
+	}
+
+	if _, err := g.createOrUpdateFn(ctx, encryptedSecret); err != nil {
+		return fmt.Errorf("failed to create secret: %w", err)
+	}
+
+	return nil
+}
+
+func (g *Client) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
+	return nil, fmt.Errorf("not implemented - this provider supports write-only operations")
+}
+
+func (g *Client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	return nil, fmt.Errorf("not implemented - this provider supports write-only operations")
+}
+
+func (g *Client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+	return nil, fmt.Errorf("not implemented - this provider supports write-only operations")
+}
+
+func (g *Client) Close(ctx context.Context) error {
+	ctx.Done()
+	return nil
+}
+
+func (g *Client) Validate() (esv1beta1.ValidationResult, error) {
+	if g.store.GetKind() == esv1beta1.ClusterSecretStoreKind {
+		return esv1beta1.ValidationResultUnknown, nil
+	}
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	defer cancel()
+	_, _, err := g.listSecretsFn(ctx)
+
+	if err != nil {
+		return esv1beta1.ValidationResultError, fmt.Errorf("store is not allowed to list secrets: %w", err)
+	}
+
+	return esv1beta1.ValidationResultReady, nil
+}

+ 216 - 0
pkg/provider/github/client_test.go

@@ -0,0 +1,216 @@
+// /*
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//	http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// */
+package github
+
+import (
+	"context"
+	"errors"
+	"testing"
+
+	github "github.com/google/go-github/v56/github"
+	"github.com/stretchr/testify/assert"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/utils/ptr"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+)
+
+type getSecretFn func(ctx context.Context, ref esv1beta1.PushSecretRemoteRef) (*github.Secret, *github.Response, error)
+
+func withGetSecretFn(secret *github.Secret, response *github.Response, err error) getSecretFn {
+	return func(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (*github.Secret, *github.Response, error) {
+		return secret, response, err
+	}
+}
+
+type getPublicKeyFn func(ctx context.Context) (*github.PublicKey, *github.Response, error)
+
+func withGetPublicKeyFn(key *github.PublicKey, response *github.Response, err error) getPublicKeyFn {
+	return func(_ context.Context) (*github.PublicKey, *github.Response, error) {
+		return key, response, err
+	}
+}
+
+type createOrUpdateSecretFn func(ctx context.Context, encryptedSecret *github.EncryptedSecret) (*github.Response, error)
+
+func withCreateOrUpdateSecretFn(response *github.Response, err error) createOrUpdateSecretFn {
+	return func(_ context.Context, _ *github.EncryptedSecret) (*github.Response, error) {
+		return response, err
+	}
+}
+
+func TestSecretExists(t *testing.T) {
+	type testCase struct {
+		name        string
+		prov        *esv1beta1.GithubProvider
+		remoteRef   esv1beta1.PushSecretData
+		getSecretFn getSecretFn
+		wantErr     error
+		exists      bool
+	}
+	tests := []testCase{
+		{
+			name:        "getSecret fail",
+			getSecretFn: withGetSecretFn(nil, nil, errors.New("boom")),
+			exists:      false,
+			wantErr:     errors.New("error fetching secret"),
+		},
+		{
+			name:        "no secret",
+			getSecretFn: withGetSecretFn(nil, nil, nil),
+			exists:      false,
+		},
+		{
+			name:        "with secret",
+			getSecretFn: withGetSecretFn(&github.Secret{}, nil, nil),
+			exists:      true,
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			g := Client{
+				provider: test.prov,
+			}
+			g.getSecretFn = test.getSecretFn
+			ok, err := g.SecretExists(context.TODO(), test.remoteRef)
+			assert.Equal(t, test.exists, ok)
+			if test.wantErr == nil {
+				assert.NoError(t, err)
+			} else {
+				assert.ErrorContains(t, err, test.wantErr.Error())
+			}
+		})
+	}
+}
+
+func TestPushSecret(t *testing.T) {
+	type testCase struct {
+		name             string
+		prov             *esv1beta1.GithubProvider
+		secret           *corev1.Secret
+		remoteRef        esv1beta1.PushSecretData
+		getSecretFn      getSecretFn
+		getPublicKeyFn   getPublicKeyFn
+		createOrUpdateFn createOrUpdateSecretFn
+		wantErr          error
+	}
+	tests := []testCase{
+		{
+			name:        "failGetSecretFn",
+			getSecretFn: withGetSecretFn(nil, nil, errors.New("boom")),
+			wantErr:     errors.New("error fetching secret"),
+		},
+		{
+			name: "failGetPublicKey",
+			getSecretFn: withGetSecretFn(&github.Secret{
+				Name: "foo",
+			}, nil, nil),
+			getPublicKeyFn: withGetPublicKeyFn(nil, nil, errors.New("boom")),
+			wantErr:        errors.New("error fetching public key"),
+		},
+		{
+			name: "failDecodeKey",
+			getSecretFn: withGetSecretFn(&github.Secret{
+				Name: "foo",
+			}, nil, nil),
+			getPublicKeyFn: withGetPublicKeyFn(&github.PublicKey{
+				Key:   ptr.To("broken"),
+				KeyID: ptr.To("123"),
+			}, nil, nil),
+			wantErr: errors.New("unable to decode public key"),
+		},
+		{
+			name: "failSecretData",
+			getSecretFn: withGetSecretFn(&github.Secret{
+				Name: "foo",
+			}, nil, nil),
+			getPublicKeyFn: withGetPublicKeyFn(&github.PublicKey{
+				Key:   ptr.To("Cg=="),
+				KeyID: ptr.To("123"),
+			}, nil, nil),
+			secret: &corev1.Secret{
+				Data: map[string][]byte{
+					"foo": []byte("bar"),
+				},
+			},
+			remoteRef: esv1alpha1.PushSecretData{
+				Match: esv1alpha1.PushSecretMatch{
+					SecretKey: "bar",
+				},
+			},
+			wantErr: errors.New("not found in secret"),
+		},
+		{
+			name: "failSecretData",
+			getSecretFn: withGetSecretFn(&github.Secret{
+				Name: "foo",
+			}, nil, nil),
+			getPublicKeyFn: withGetPublicKeyFn(&github.PublicKey{
+				Key:   ptr.To("Zm9vYmFyCg=="),
+				KeyID: ptr.To("123"),
+			}, nil, nil),
+			secret: &corev1.Secret{
+				Data: map[string][]byte{
+					"foo": []byte("bingg"),
+				},
+			},
+			remoteRef: esv1alpha1.PushSecretData{
+				Match: esv1alpha1.PushSecretMatch{
+					SecretKey: "foo",
+				},
+			},
+			createOrUpdateFn: withCreateOrUpdateSecretFn(nil, errors.New("boom")),
+			wantErr:          errors.New("failed to create secret"),
+		},
+		{
+			name: "Success",
+			getSecretFn: withGetSecretFn(&github.Secret{
+				Name: "foo",
+			}, nil, nil),
+			getPublicKeyFn: withGetPublicKeyFn(&github.PublicKey{
+				Key:   ptr.To("Zm9vYmFyCg=="),
+				KeyID: ptr.To("123"),
+			}, nil, nil),
+			secret: &corev1.Secret{
+				Data: map[string][]byte{
+					"foo": []byte("bingg"),
+				},
+			},
+			remoteRef: esv1alpha1.PushSecretData{
+				Match: esv1alpha1.PushSecretMatch{
+					SecretKey: "foo",
+				},
+			},
+			createOrUpdateFn: withCreateOrUpdateSecretFn(nil, nil),
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			g := Client{
+				provider: test.prov,
+			}
+			g.getSecretFn = test.getSecretFn
+			g.getPublicKeyFn = test.getPublicKeyFn
+			g.createOrUpdateFn = test.createOrUpdateFn
+			err := g.PushSecret(context.TODO(), test.secret, test.remoteRef)
+			if test.wantErr == nil {
+				assert.NoError(t, err)
+			} else {
+				assert.ErrorContains(t, err, test.wantErr.Error())
+			}
+		})
+	}
+}

+ 42 - 0
pkg/provider/github/env_secrets.go

@@ -0,0 +1,42 @@
+// /*
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//	http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// */
+package github
+
+import (
+	"context"
+
+	github "github.com/google/go-github/v56/github"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+)
+
+func (g *Client) envGetSecretFn(ctx context.Context, ref esv1beta1.PushSecretRemoteRef) (*github.Secret, *github.Response, error) {
+	return g.baseClient.GetEnvSecret(ctx, int(g.repoID), g.provider.Environment, ref.GetRemoteKey())
+}
+
+func (g *Client) envGetPublicKeyFn(ctx context.Context) (*github.PublicKey, *github.Response, error) {
+	return g.baseClient.GetEnvPublicKey(ctx, int(g.repoID), g.provider.Environment)
+}
+
+func (g *Client) envCreateOrUpdateSecret(ctx context.Context, encryptedSecret *github.EncryptedSecret) (*github.Response, error) {
+	return g.baseClient.CreateOrUpdateEnvSecret(ctx, int(g.repoID), g.provider.Environment, encryptedSecret)
+}
+
+func (g *Client) envListSecretsFn(ctx context.Context) (*github.Secrets, *github.Response, error) {
+	return g.baseClient.ListEnvSecrets(ctx, int(g.repoID), g.provider.Environment, &github.ListOptions{})
+}
+
+func (g *Client) envDeleteSecretsFn(ctx context.Context, remoteRef esv1beta1.PushSecretRemoteRef) (*github.Response, error) {
+	return g.baseClient.DeleteEnvSecret(ctx, int(g.repoID), g.provider.Environment, remoteRef.GetRemoteKey())
+}

+ 42 - 0
pkg/provider/github/org_secrets.go

@@ -0,0 +1,42 @@
+// /*
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//	http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// */
+package github
+
+import (
+	"context"
+
+	github "github.com/google/go-github/v56/github"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+)
+
+func (g *Client) orgGetSecretFn(ctx context.Context, ref esv1beta1.PushSecretRemoteRef) (*github.Secret, *github.Response, error) {
+	return g.baseClient.GetOrgSecret(ctx, g.provider.Organization, ref.GetRemoteKey())
+}
+
+func (g *Client) orgGetPublicKeyFn(ctx context.Context) (*github.PublicKey, *github.Response, error) {
+	return g.baseClient.GetOrgPublicKey(ctx, g.provider.Organization)
+}
+
+func (g *Client) orgCreateOrUpdateSecret(ctx context.Context, encryptedSecret *github.EncryptedSecret) (*github.Response, error) {
+	return g.baseClient.CreateOrUpdateOrgSecret(ctx, g.provider.Organization, encryptedSecret)
+}
+
+func (g *Client) orgListSecretsFn(ctx context.Context) (*github.Secrets, *github.Response, error) {
+	return g.baseClient.ListOrgSecrets(ctx, g.provider.Organization, &github.ListOptions{})
+}
+
+func (g *Client) orgDeleteSecretsFn(ctx context.Context, remoteRef esv1beta1.PushSecretRemoteRef) (*github.Response, error) {
+	return g.baseClient.DeleteOrgSecret(ctx, g.provider.Organization, remoteRef.GetRemoteKey())
+}

+ 129 - 0
pkg/provider/github/provider.go

@@ -0,0 +1,129 @@
+// /*
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//	http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// */
+package github
+
+import (
+	"context"
+	"errors"
+	"fmt"
+
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+)
+
+const (
+	errUnexpectedStoreSpec = "unexpected store spec"
+	errInvalidStoreSpec    = "invalid store spec"
+	errInvalidStoreProv    = "invalid store provider"
+	errInvalidGithubProv   = "invalid github provider"
+	errInvalidStore        = "invalid store"
+	errInvalidProvider     = "invalid provider"
+)
+
+type Provider struct {
+}
+
+var _ esv1beta1.Provider = &Provider{}
+
+func init() {
+	esv1beta1.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
+		Github: &esv1beta1.GithubProvider{},
+	})
+}
+
+// Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
+func (p *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
+	return esv1beta1.SecretStoreWriteOnly
+}
+
+// NewClient constructs a new secrets client based on the provided store.
+func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
+	return newClient(ctx, store, kube, namespace)
+}
+
+func newClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
+	provider, err := getProvider(store)
+	if err != nil {
+		return nil, err
+	}
+	g := &Client{
+		crClient:  kube,
+		store:     store,
+		namespace: namespace,
+		provider:  provider,
+		storeKind: store.GetObjectKind().GroupVersionKind().Kind,
+	}
+	g.getSecretFn = g.orgGetSecretFn
+	g.getPublicKeyFn = g.orgGetPublicKeyFn
+	g.createOrUpdateFn = g.orgCreateOrUpdateSecret
+	g.listSecretsFn = g.orgListSecretsFn
+	g.deleteSecretFn = g.orgDeleteSecretsFn
+	client, err := g.AuthWithPrivateKey(ctx)
+	if err != nil {
+		return nil, fmt.Errorf("could not get private key: %w", err)
+	}
+	g.baseClient = *client.Actions
+	if provider.Repository != "" {
+		g.getSecretFn = g.repoGetSecretFn
+		g.getPublicKeyFn = g.repoGetPublicKeyFn
+		g.createOrUpdateFn = g.repoCreateOrUpdateSecret
+		g.listSecretsFn = g.repoListSecretsFn
+		g.deleteSecretFn = g.repoDeleteSecretsFn
+		if provider.Environment != "" {
+			// For environment to work, we need the repository ID instead of its name.
+			repository, _, err := client.Repositories.Get(ctx, g.provider.Organization, g.provider.Repository)
+			if err != nil {
+				return nil, fmt.Errorf("error fetching repository: %w", err)
+			}
+			g.repoID = repository.GetID()
+			g.getSecretFn = g.envGetSecretFn
+			g.getPublicKeyFn = g.envGetPublicKeyFn
+			g.createOrUpdateFn = g.envCreateOrUpdateSecret
+			g.listSecretsFn = g.envListSecretsFn
+			g.deleteSecretFn = g.envDeleteSecretsFn
+		}
+	}
+
+	return g, nil
+}
+
+func getProvider(store esv1beta1.GenericStore) (*esv1beta1.GithubProvider, error) {
+	spc := store.GetSpec()
+	if spc == nil || spc.Provider.Github == nil {
+		return nil, errors.New(errUnexpectedStoreSpec)
+	}
+
+	return spc.Provider.Github, nil
+}
+
+func (p *Provider) ValidateStore(store esv1beta1.GenericStore) (admission.Warnings, error) {
+	if store == nil {
+		return nil, errors.New(errInvalidStore)
+	}
+	spc := store.GetSpec()
+	if spc == nil {
+		return nil, errors.New(errInvalidStoreSpec)
+	}
+	if spc.Provider == nil {
+		return nil, errors.New(errInvalidStoreProv)
+	}
+	prov := spc.Provider.Github
+	if prov == nil {
+		return nil, errors.New(errInvalidGithubProv)
+	}
+
+	return nil, nil
+}

+ 42 - 0
pkg/provider/github/repo_secrets.go

@@ -0,0 +1,42 @@
+// /*
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//	http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// */
+package github
+
+import (
+	"context"
+
+	github "github.com/google/go-github/v56/github"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+)
+
+func (g *Client) repoGetSecretFn(ctx context.Context, ref esv1beta1.PushSecretRemoteRef) (*github.Secret, *github.Response, error) {
+	return g.baseClient.GetRepoSecret(ctx, g.provider.Organization, g.provider.Repository, ref.GetRemoteKey())
+}
+
+func (g *Client) repoGetPublicKeyFn(ctx context.Context) (*github.PublicKey, *github.Response, error) {
+	return g.baseClient.GetRepoPublicKey(ctx, g.provider.Organization, g.provider.Repository)
+}
+
+func (g *Client) repoCreateOrUpdateSecret(ctx context.Context, encryptedSecret *github.EncryptedSecret) (*github.Response, error) {
+	return g.baseClient.CreateOrUpdateRepoSecret(ctx, g.provider.Organization, g.provider.Repository, encryptedSecret)
+}
+
+func (g *Client) repoListSecretsFn(ctx context.Context) (*github.Secrets, *github.Response, error) {
+	return g.baseClient.ListRepoSecrets(ctx, g.provider.Organization, g.provider.Repository, &github.ListOptions{})
+}
+
+func (g *Client) repoDeleteSecretsFn(ctx context.Context, remoteRef esv1beta1.PushSecretRemoteRef) (*github.Response, error) {
+	return g.baseClient.DeleteRepoSecret(ctx, g.provider.Organization, g.provider.Environment, remoteRef.GetRemoteKey())
+}

+ 1 - 0
pkg/provider/register/register.go

@@ -31,6 +31,7 @@ import (
 	_ "github.com/external-secrets/external-secrets/pkg/provider/fake"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/fake"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/fortanix"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/fortanix"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/gcp/secretmanager"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/gcp/secretmanager"
+	_ "github.com/external-secrets/external-secrets/pkg/provider/github"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/gitlab"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/gitlab"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/ibm"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/ibm"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/infisical"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/infisical"

+ 1 - 1
tests/__snapshot__/clustergenerator-v1alpha1.yaml

@@ -272,4 +272,4 @@ spec:
           name: string
           name: string
       timeout: string
       timeout: string
       url: string
       url: string
-  kind: "ACRAccessToken" # "ACRAccessToken", "ECRAuthorizationToken", "Fake", "GCRAccessToken", "GithubAccessToken", "QuayAccessToken'Password", "STSSessionToken", "UUID", "VaultDynamicSecret", "Webhook", "Grafana"
+  kind: "ACRAccessToken" # "ACRAccessToken", "ECRAuthorizationToken", "Fake", "GCRAccessToken", "GithubAccessToken", "QuayAccessToken", "Password", "STSSessionToken", "UUID", "VaultDynamicSecret", "Webhook", "Grafana"

+ 13 - 0
tests/__snapshot__/clustersecretstore-v1beta1.yaml

@@ -285,6 +285,19 @@ spec:
             namespace: string
             namespace: string
       location: string
       location: string
       projectID: string
       projectID: string
+    github:
+      appID: 1
+      auth:
+        privateKey:
+          key: string
+          name: string
+          namespace: string
+      environment: string
+      installationID: 1
+      organization: string
+      repository: string
+      uploadURL: string
+      url: "https://github.com/"
     gitlab:
     gitlab:
       auth:
       auth:
         SecretRef:
         SecretRef:

+ 1 - 1
tests/__snapshot__/pushsecret-v1alpha1.yaml

@@ -11,7 +11,7 @@ spec:
       secretKey: string
       secretKey: string
     metadata: 
     metadata: 
   deletionPolicy: "None"
   deletionPolicy: "None"
-  refreshInterval: string
+  refreshInterval: "1h"
   secretStoreRefs:
   secretStoreRefs:
   - kind: "SecretStore"
   - kind: "SecretStore"
     labelSelector:
     labelSelector:

+ 13 - 0
tests/__snapshot__/secretstore-v1beta1.yaml

@@ -285,6 +285,19 @@ spec:
             namespace: string
             namespace: string
       location: string
       location: string
       projectID: string
       projectID: string
+    github:
+      appID: 1
+      auth:
+        privateKey:
+          key: string
+          name: string
+          namespace: string
+      environment: string
+      installationID: 1
+      organization: string
+      repository: string
+      uploadURL: string
+      url: "https://github.com/"
     gitlab:
     gitlab:
       auth:
       auth:
         SecretRef:
         SecretRef: