feat: SSH CA

This commit is contained in:
Kat Inskip 2023-02-04 14:18:40 -08:00
parent a28e1ce6e2
commit ccf6a6f704
Signed by: kat
GPG key ID: 465E64DECEA8CF0F
23 changed files with 678 additions and 431 deletions

8
.idea/.gitignore generated vendored Normal file
View file

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

9
.idea/kittywitch.iml generated Normal file
View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml generated Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/kittywitch.iml" filepath="$PROJECT_DIR$/.idea/kittywitch.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View file

@ -1,6 +1,6 @@
config:
cloudflare:apiToken:
secure: AAABACPPwldgPSVHPYdh4lQnYR+baaGsnQRwGw/qwNke8wuFnCTdZIqj6yXu0CCGaYBu0+66LTALNNl9mo9HXs/PMLmoOqLo
secure: AAABAFcufTX7tZZf2gcK6hML2tgovDEfcPAJcgjfYkV3GMS4Ilwzuco5p+hCpyj3vCm7cqm3tmdwOLlOFxGqKZGRj+ESXAzv
tailscale:apiKey:
secure: AAABAGc7s7XJ+voSUNcMmRuVwrUdx3kojn0fdEl6qpUy0WmhgHbk6cEz2/kGSEGhuLGwo3mzOGVTI+NVu6/Xz4PmE9FME++VfE8cz5DFjDrMJ4JdX0DR
tailscale:tailnet: inskip.me

View file

@ -6,6 +6,8 @@ zones:
flags: 0
tag: iodef
value: mailto:acme@inskip.me
- kind: cname
value: inskip-root.pages.dev
- kind: caa
flags: 0
tag: issue

View file

@ -1,5 +1,16 @@
_: {
{ pkgs, ... }: {
nix.envVars = {
"SSH_AUTH_SOCK" = "/Users/kat/.gnupg/S.gpg-agent.ssh";
};
launchd.daemons.start_nixos_native = {
serviceConfig.ProgramArguments = [
"/bin/sh" "-c"
"/bin/wait4path /nix/store &amp;&amp; ${pkgs.writeScript "start_nixos_native" ''
/usr/bin/open "utm://start?name=NixOS Native"
''}"
];
serviceConfig.Label = "org.kittywitch.start_nixos_native";
serviceConfig.RunAtLoad = true;
};
}

30
flake.lock generated
View file

@ -24,11 +24,11 @@
"arcexprs": {
"flake": false,
"locked": {
"lastModified": 1674878824,
"narHash": "sha256-skuyKtydzGFtd2UQB2BZW2bMaUcS+fvHEEz+H4MNQ4g=",
"lastModified": 1675107003,
"narHash": "sha256-ao5OLwhC7+T3O2ixRrt76gIg9uZR3c17SJL3Wvx0EpQ=",
"owner": "arcnmx",
"repo": "nixexprs",
"rev": "ab1bd348da95d6f33ad28992b5d8c636d2330cc9",
"rev": "5b7d1eb5e578da7ed36b2105f80f82f9ad11244d",
"type": "github"
},
"original": {
@ -126,11 +126,11 @@
]
},
"locked": {
"lastModified": 1674771519,
"narHash": "sha256-U0W3S1nX6yEvLh3Vq70EORbmXecAKXfmEfCfaA4A+I8=",
"lastModified": 1675181178,
"narHash": "sha256-jymSUUjKoArptU7LJ1i4boysXptnpuETiUTenKgs2fM=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "bb4b25b302dbf0f527f190461b080b5262871756",
"rev": "69696fe53940562a047bf2ec675cc1dcd1bc09b3",
"type": "github"
},
"original": {
@ -173,11 +173,11 @@
]
},
"locked": {
"lastModified": 1674357402,
"narHash": "sha256-oxOCYORXBh0KW4KEwKpzs2LThDdVmEwREV+SsP4CIpg=",
"lastModified": 1674962474,
"narHash": "sha256-qEXdgW5fnMSdQwP1zQYa0fVtI0f3G1f2qNRjUEherCs=",
"owner": "Mic92",
"repo": "nix-index-database",
"rev": "e9e7c5c62965e7e656febb5ba578d53f751eb41f",
"rev": "a385f6192f5471c4cebeeb0d2e966b5ccf123df5",
"type": "github"
},
"original": {
@ -188,11 +188,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1674641431,
"narHash": "sha256-qfo19qVZBP4qn5M5gXc/h1MDgAtPA5VxJm9s8RUAkVk=",
"lastModified": 1675115703,
"narHash": "sha256-4zetAPSyY0D77x+Ww9QBe8RHn1akvIvHJ/kgg8kGDbk=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "9b97ad7b4330aacda9b2343396eb3df8a853b4fc",
"rev": "2caf4ef5005ecc68141ecb4aac271079f7371c44",
"type": "github"
},
"original": {
@ -205,11 +205,11 @@
"pypi-deps-db": {
"flake": false,
"locked": {
"lastModified": 1674855014,
"narHash": "sha256-SMUq72uWWs+KAlcRB7UXzI1QHNTGiUTpQK2qj1evHXQ=",
"lastModified": 1675156620,
"narHash": "sha256-lmnsBYJz2Fgm0WFNUgSqskpwa0ffbFOr9YGDZpUXptk=",
"owner": "DavHau",
"repo": "pypi-deps-db",
"rev": "a375715227007ca768d372b2b09bcb76f8f19d78",
"rev": "2e236bb32e6e3ef13cd56fc6d9aee8c89059a1ac",
"type": "github"
},
"original": {

View file

@ -1,22 +1,27 @@
package iac
import(
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
import (
tls "github.com/pulumi/pulumi-tls/sdk/v4/go/tls"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func GenerateTLSCA(ctx *pulumi.Context) (key *tls.PrivateKey, cert *tls.SelfSignedCert, err error) {
key, err = tls.NewPrivateKey(ctx, "kat-root-ca-key", &tls.PrivateKeyArgs{
type CertificateAuthority struct {
Key *tls.PrivateKey
Cert *tls.SelfSignedCert
}
func (ca *CertificateAuthority) handle(ctx *pulumi.Context) (err error) {
ca.Key, err = tls.NewPrivateKey(ctx, "ca-root", &tls.PrivateKeyArgs{
Algorithm: pulumi.String("RSA"),
RsaBits: pulumi.Int(4096),
})
if err != nil {
return nil, nil, err
return err
}
cert, err = tls.NewSelfSignedCert(ctx, "kat-root-ca-pem-cert", &tls.SelfSignedCertArgs{
PrivateKeyPem: key.PrivateKeyPem,
ca.Cert, err = tls.NewSelfSignedCert(ctx, "ca-root", &tls.SelfSignedCertArgs{
PrivateKeyPem: ca.Key.PrivateKeyPem,
AllowedUses: goStringArrayToPulumiStringArray([]string{"digital_signature",
"cert_signing",
"crl_signing"}),
@ -29,12 +34,12 @@ func GenerateTLSCA(ctx *pulumi.Context) (key *tls.PrivateKey, cert *tls.SelfSign
})
if err != nil {
return nil, nil, err
return err
}
ctx.Export("tls_ca_pem_key", key.PrivateKeyPem)
ctx.Export("tls_ca_os_key", key.PrivateKeyOpenssh)
ctx.Export("tls_ca_cert", cert.CertPem)
ctx.Export("ca_pem_privkey", ca.Key.PrivateKeyPem)
ctx.Export("ca_os_privkey", ca.Key.PrivateKeyOpenssh)
ctx.Export("ca_pem_cert", ca.Cert.CertPem)
return key, cert, err
return err
}

205
iac/device.go Normal file
View file

@ -0,0 +1,205 @@
package iac
import (
"crypto/rand"
"fmt"
"github.com/pulumi/pulumi-command/sdk/go/command/remote"
"github.com/pulumi/pulumi-tailscale/sdk/go/tailscale"
"github.com/pulumi/pulumi-tls/sdk/v4/go/tls"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
"golang.org/x/crypto/ssh"
"net"
"strings"
"time"
)
type Tailnet struct {
Devices []Device
Zone *Zone
}
func (t *Tailnet) handle(ctx *pulumi.Context, zone *Zone, CAKey *tls.PrivateKey, CACert *tls.SelfSignedCert) (err error) {
t.Zone = zone
tailnet, err := tailscale.GetDevices(ctx, &tailscale.GetDevicesArgs{}, nil)
if err != nil {
return err
}
for _, device := range tailnet.Devices {
device_ := Device{
Addresses: device.Addresses,
Id: device.Id,
Name: device.Name,
Tags: device.Tags,
User: device.User,
}
err = device_.handle(ctx, zone, CAKey, CACert)
if err != nil {
return err
}
t.Devices = append(t.Devices, device_)
}
return err
}
type Device struct {
Addresses []string
Id string
Name string
Hostname string
Tailskip string
Tags []string
User string
Files []*remote.Command
Context *pulumi.Context
Records []DNSRecord
PrivateKey *tls.PrivateKey
TLSCertRequest *tls.CertRequest
TLSCert *tls.LocallySignedCert
OSHCertificate pulumi.StringOutput
}
func (d *Device) handle(ctx *pulumi.Context, zone *Zone, CAKey *tls.PrivateKey, CACert *tls.SelfSignedCert) (err error) {
d.Context = ctx
d.Records = make([]DNSRecord, 0, 50)
d.Files = make([]*remote.Command, 0, 20)
d.Hostname = strings.Split(d.Name, ".")[0]
d.Tailskip = fmt.Sprintf("%s.inskip.me", d.Hostname)
if d.User != "kat@inskip.me" {
return nil
}
err = d.record(zone)
if err != nil {
return err
}
if d.Hostname != "koishi" {
return err
}
err = d.handleTLS(CAKey, CACert)
if err != nil {
return err
}
err = d.handleOSH(CAKey)
if err != nil {
return err
}
return err
}
func (d *Device) handleOSH(CAKey *tls.PrivateKey) (err error) {
d.OSHCertificate = CAKey.PrivateKeyOpenssh.ApplyT(func(CAPriv string) pulumi.StringOutput {
OSHCertificate_ := d.PrivateKey.PrivateKeyOpenssh.ApplyT(func(UserPriv string) pulumi.String {
CARSAPriv, err := PrivateKeyOpenSSHToRSAPrivateKey(CAPriv)
if err != nil {
panic(err)
}
signer, err := ssh.NewSignerFromKey(CARSAPriv)
if err != nil {
panic(err)
}
var cert ssh.Certificate
cert.Nonce = make([]byte, 32)
cert.CertType = 2
UserRSAPriv, err := PrivateKeyOpenSSHToRSAPrivateKey(UserPriv)
if err != nil {
panic(err)
}
cert.Key, err = ssh.NewPublicKey(UserRSAPriv.Public())
if err != nil {
panic(err)
}
cert.Serial = 0
cert.KeyId = d.Tailskip
cert.ValidPrincipals = []string{d.Tailskip}
cert.ValidAfter = 60
threeMonths, err := time.ParseDuration("730h")
if err != nil {
panic(err)
}
threeMonthsInSeconds := uint64(threeMonths.Seconds())
cert.ValidBefore = threeMonthsInSeconds
err = cert.SignCert(rand.Reader, signer)
return pulumi.String(string(ssh.MarshalAuthorizedKey(&cert)))
}).(pulumi.StringOutput)
return OSHCertificate_
}).(pulumi.StringOutput)
file, err := CreatePulumiFile(d.Context, fmt.Sprintf("%s-osh-cert", d.Hostname), d.Tailskip, d.OSHCertificate, []pulumi.Resource{d.PrivateKey, CAKey})
d.Files = append(d.Files, file)
return err
}
func (d *Device) record(zone *Zone) (err error) {
for _, address := range d.Addresses {
ip := net.ParseIP(address)
kind := A
if ip.To4() == nil {
kind = AAAA
}
record := DNSRecord{
Name: d.Hostname,
Kind: kind,
Value: ip.String(),
Ttl: 3600,
}
err = record.handle(d.Context, zone)
if err != nil {
return err
}
d.Records = append(d.Records, record)
}
return err
}
func (d *Device) handleTLS(CAKey *tls.PrivateKey, CACert *tls.SelfSignedCert) (err error) {
PrivateKeyDepends := []pulumi.Resource{CAKey, CACert}
d.PrivateKey, err = tls.NewPrivateKey(d.Context, fmt.Sprintf("%s-key", d.Hostname), &tls.PrivateKeyArgs{
Algorithm: pulumi.String("RSA"),
RsaBits: pulumi.Int(4096),
}, pulumi.DependsOn(PrivateKeyDepends))
if err != nil {
return err
}
PrivateKeyDepends = append(PrivateKeyDepends, d.PrivateKey)
file, err := CreatePulumiFile(d.Context, fmt.Sprintf("%s-pem-pk", d.Hostname), d.Tailskip, d.PrivateKey.PrivateKeyPem, PrivateKeyDepends)
if err != nil {
return err
}
d.Files = append(d.Files, file)
file, err = CreatePulumiFile(d.Context, fmt.Sprintf("%s-osh-pk", d.Hostname), d.Tailskip, d.PrivateKey.PrivateKeyOpenssh, PrivateKeyDepends)
if err != nil {
return err
}
d.Files = append(d.Files, file)
TLSCertRequestDepends := []pulumi.Resource{CAKey, CACert, d.PrivateKey}
d.TLSCertRequest, err = tls.NewCertRequest(d.Context, fmt.Sprintf("%s-tls-cr", d.Hostname), &tls.CertRequestArgs{
PrivateKeyPem: d.PrivateKey.PrivateKeyPem,
DnsNames: goStringArrayToPulumiStringArray([]string{d.Hostname}),
IpAddresses: goStringArrayToPulumiStringArray(d.Addresses),
Subject: &tls.CertRequestSubjectArgs{
CommonName: pulumi.String("inskip.me"),
Organization: pulumi.String("Kat Inskip"),
},
}, pulumi.DependsOn(TLSCertRequestDepends))
if err != nil {
return err
}
TLSCertDepends := []pulumi.Resource{CAKey, CACert, d.TLSCertRequest, d.PrivateKey}
d.TLSCert, err = tls.NewLocallySignedCert(d.Context, fmt.Sprintf("%s-tls-cert", d.Hostname), &tls.LocallySignedCertArgs{
AllowedUses: goStringArrayToPulumiStringArray([]string{"digital_signature",
"digital_signature",
"key_encipherment",
"key_agreement",
"email_protection",
}),
CaPrivateKeyPem: CAKey.PrivateKeyPem,
CaCertPem: CACert.CertPem,
CertRequestPem: d.TLSCertRequest.CertRequestPem,
ValidityPeriodHours: pulumi.Int(1440),
EarlyRenewalHours: pulumi.Int(168),
}, pulumi.DependsOn(TLSCertDepends))
file, err = CreatePulumiFile(d.Context, fmt.Sprintf("%s-pem-cert", d.Hostname), d.Tailskip, d.TLSCert.CertPem, TLSCertDepends)
d.Files = append(d.Files, file)
if err != nil {
return err
}
return err
}

View file

@ -1,37 +0,0 @@
package iac
import(
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
cloudflare "github.com/pulumi/pulumi-cloudflare/sdk/v4/go/cloudflare"
"fmt"
)
func HandleDNS(ctx *pulumi.Context, config KatConfig) (zones map[string]*cloudflare.Zone, dnssec map[string]*cloudflare.ZoneDnssec, records map[string]*cloudflare.Record, err error) {
zones = make(map[string]*cloudflare.Zone)
dnssec = make(map[string]*cloudflare.ZoneDnssec)
records = make(map[string]*cloudflare.Record)
for name, zone := range config.Zones {
ctx.Log.Info(fmt.Sprintf("Handling zone %s", name), nil)
zones[name], err = zone.handle(ctx, name)
if err != nil {
return nil, nil, nil, err
}
dnssec[name], err = cloudflare.NewZoneDnssec(ctx, fmt.Sprintf("%s-dnssec", name), &cloudflare.ZoneDnssecArgs{
ZoneId: zones[name].ID(),
})
if err != nil {
return nil, nil, nil, err
}
for _, record := range zone.Records {
record_, err := record.handle(ctx, name, zones[name])
if err != nil {
return nil, nil, nil, err
}
record_index := record.getName(name, zones[name])
records[record_index] = record_
}
}
return zones, dnssec, records, err
}

View file

@ -1,43 +1,23 @@
package iac
import (
"github.com/pulumi/pulumi-command/sdk/go/command/local"
"github.com/pulumi/pulumi-command/sdk/go/command/remote"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
"github.com/pulumi/pulumi-tls/sdk/v4/go/tls"
"fmt"
"os"
"path"
)
func createPulumiFile(ctx *pulumi.Context, name string, value pulumi.StringOutput, resource pulumi.Resource) (*local.Command, error) {
repo_root := os.Getenv("REPO_ROOT")
data_root := path.Join(repo_root, "./data")
ctx.Export(name, value)
return local.NewCommand(ctx, name, &local.CommandArgs{
Create: pulumi.String(fmt.Sprintf("pulumi stack output %s --non-interactive --show-secrets > %s", name, name)),
Update: pulumi.String(fmt.Sprintf("pulumi stack output %s --non-interactive --show-secrets > %s", name, name)),
Delete: pulumi.String(fmt.Sprintf("rm %s", name)),
Dir: pulumi.String(data_root),
Environment: goMapToPulumiMap(map[string]string{
func CreatePulumiFile(ctx *pulumi.Context, name string, fqdn string, value pulumi.StringOutput, resources []pulumi.Resource) (*remote.Command, error) {
environment := goMapToPulumiMap(map[string]string{
"PULUMI_SKIP_UPDATE_CHECK": "true",
}),
}, pulumi.DependsOn([]pulumi.Resource{resource}))
}
func PKITLSFiles(ctx *pulumi.Context, files_ map[string]*local.Command, keys map[string]*tls.PrivateKey, certs map[string]*tls.LocallySignedCert) (files map[string]*local.Command, err error) {
for name_, key := range keys {
name := fmt.Sprintf("%s-file", name_)
files_[name], err = createPulumiFile(ctx, name, key.PrivateKeyPem, key)
if err != nil {
return nil, err
}
}
for name_, cert := range certs {
name := fmt.Sprintf("%s-file", name_)
files_[name], err = createPulumiFile(ctx, name, cert.CertPem, cert)
if err != nil {
return nil, err
}
}
return files_, err
})
return remote.NewCommand(ctx, name, &remote.CommandArgs{
Connection: &remote.ConnectionArgs{
Host: pulumi.String(fqdn),
Port: pulumi.Float64Ptr(22),
User: pulumi.String("deploy"),
AgentSocketPath: pulumi.String("/Users/kat/.gnupg/S.gpg-agent.ssh"),
},
Create: pulumi.Sprintf("sudo mkdir -p /var/lib/secrets && sudo chown deploy:users -R /var/lib/secrets && cd /var/lib/secrets && echo \"%s\" > \"%s\"", value, name),
Delete: pulumi.Sprintf("cd /var/lib/secrets && rm %s", name),
Environment: environment,
}, pulumi.DependsOn(resources))
}

93
iac/inskip.go Normal file
View file

@ -0,0 +1,93 @@
package iac
import (
"github.com/pulumi/pulumi-cloudflare/sdk/v4/go/cloudflare"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func InskipPage(ctx *pulumi.Context) error {
_, err := cloudflare.NewPagesProject(ctx, "inskip-root", &cloudflare.PagesProjectArgs{
AccountId: pulumi.ID("0467b993b65d8fd4a53fe24ed2fbb2a1"),
Name: pulumi.String("inskip-root"),
ProductionBranch: pulumi.String("main"),
BuildConfig: &cloudflare.PagesProjectBuildConfigArgs{
BuildCommand: pulumi.String("hugo"),
DestinationDir: pulumi.String("public"),
RootDir: pulumi.String("/"),
},
Source: &cloudflare.PagesProjectSourceArgs{
Type: pulumi.String("github"),
Config: &cloudflare.PagesProjectSourceConfigArgs{
DeploymentsEnabled: pulumi.Bool(true),
Owner: pulumi.String("kittywitch"),
PrCommentsEnabled: pulumi.Bool(false),
PreviewBranchExcludes: pulumi.StringArray{
pulumi.String("main"),
pulumi.String("prod"),
},
PreviewBranchIncludes: pulumi.StringArray{
pulumi.String("dev"),
pulumi.String("preview"),
},
PreviewDeploymentSetting: pulumi.String("custom"),
ProductionBranch: pulumi.String("main"),
ProductionDeploymentEnabled: pulumi.Bool(true),
RepoName: pulumi.String("inskip.me"),
},
},
DeploymentConfigs: &cloudflare.PagesProjectDeploymentConfigsArgs{
Preview: &cloudflare.PagesProjectDeploymentConfigsPreviewArgs{
CompatibilityDate: pulumi.String("2022-08-15"),
CompatibilityFlags: pulumi.StringArray{},
/* D1Databases: pulumi.AnyMap{
"D1BINDING": pulumi.Any("445e2955-951a-4358-a35b-a4d0c813f63"),
},
DurableObjectNamespaces: pulumi.AnyMap{
"DOBINDING": pulumi.Any("5eb63bbbe01eeed093cb22bb8f5acdc3"),
},
EnvironmentVariables: pulumi.AnyMap{
"ENVIRONMENT": pulumi.Any("preview"),
},
KvNamespaces: pulumi.AnyMap{
"KVBINDING": pulumi.Any("5eb63bbbe01eeed093cb22bb8f5acdc3"),
},
R2Buckets: pulumi.AnyMap{
"R2BINDING": pulumi.Any("some-bucket"),
}, */
},
Production: &cloudflare.PagesProjectDeploymentConfigsProductionArgs{
CompatibilityDate: pulumi.String("2022-08-16"),
CompatibilityFlags: pulumi.StringArray{},
/*D1Databases: pulumi.AnyMap{
"D1BINDING1": pulumi.Any("445e2955-951a-4358-a35b-a4d0c813f63"),
"D1BINDING2": pulumi.Any("a399414b-c697-409a-a688-377db6433cd9"),
},
DurableObjectNamespaces: pulumi.AnyMap{
"DOBINDING1": pulumi.Any("5eb63bbbe01eeed093cb22bb8f5acdc3"),
"DOBINDING2": pulumi.Any("3cdca5f8bb22bc390deee10ebbb36be5"),
},
EnvironmentVariables: pulumi.AnyMap{
"ENVIRONMENT": pulumi.Any("production"),
"OTHERVALUE": pulumi.Any("other value"),
},
KvNamespaces: pulumi.AnyMap{
"KVBINDING1": pulumi.Any("5eb63bbbe01eeed093cb22bb8f5acdc3"),
"KVBINDING2": pulumi.Any("3cdca5f8bb22bc390deee10ebbb36be5"),
},
R2Buckets: pulumi.AnyMap{
"R2BINDING1": pulumi.Any("some-bucket"),
"R2BINDING2": pulumi.Any("other-bucket"),
},*/
},
},
})
_, err = cloudflare.NewPagesDomain(ctx, "inskip-root", &cloudflare.PagesDomainArgs{
AccountId: pulumi.String("0467b993b65d8fd4a53fe24ed2fbb2a1"),
Domain: pulumi.String("inskip.me"),
ProjectName: pulumi.String("inskip-root"),
})
if err != nil {
return err
}
return nil
}

View file

@ -1,13 +1,13 @@
package iac
import (
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
cloudflare "github.com/pulumi/pulumi-cloudflare/sdk/v4/go/cloudflare"
"github.com/creasty/defaults"
"fmt"
"strings"
"crypto/md5"
"encoding/hex"
"fmt"
"github.com/creasty/defaults"
"github.com/pulumi/pulumi-cloudflare/sdk/v4/go/cloudflare"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
"strings"
)
type DNSRecordType string
@ -18,9 +18,12 @@ const (
MX = "mx"
TXT = "txt"
CAA = "caa"
CNAME = "cname"
)
type DNSRecord struct {
CFRecord *cloudflare.Record
Zone *Zone
Name string `default:"@" yaml:"name"`
Kind DNSRecordType `yaml:"kind"`
Value string `yaml:"value,omitempty"`
@ -30,23 +33,27 @@ type DNSRecord struct {
Ttl int `default:"3600" yaml:"ttl,omitempty"`
}
func (r *DNSRecord) UnmarshalYAML(unmarshal func(interface{}) error) error {
defaults.Set(r)
func (r *DNSRecord) UnmarshalYAML(unmarshal func(interface{}) error) (err error) {
err = defaults.Set(r)
if err != nil {
return err
}
type plain DNSRecord
if err := unmarshal((*plain)(r)); err != nil {
return err
}
return nil
return err
}
func (r *DNSRecord) getZone(Zone *cloudflare.Zone) (pulumi.StringOutput) {
return Zone.ID().ToStringOutput()
func (r *DNSRecord) getZone() pulumi.StringOutput {
return r.Zone.CFZone.ID().ToStringOutput()
}
func (r *DNSRecord) getName(ZoneName string, Zone *cloudflare.Zone) (string) {
base := fmt.Sprintf("%s-%s-%s", ZoneName, r.Kind, r.Name)
func (r *DNSRecord) getName() string {
base := fmt.Sprintf("%s-%s-%s", r.Zone.Alias, r.Kind, r.Name)
hash := md5.Sum([]byte(r.Value))
hashString := hex.EncodeToString(hash[:])[:5]
@ -56,7 +63,7 @@ func (r *DNSRecord) getName(ZoneName string, Zone *cloudflare.Zone) (string) {
suffix = fmt.Sprintf("-%d-%s", r.Priority, hashString)
case CAA:
suffix = fmt.Sprintf("%s-%s", r.Flags, r.Tag)
case A, AAAA, TXT:
case A, AAAA, TXT, CNAME:
suffix = fmt.Sprintf("-%s", hashString)
}
@ -64,12 +71,13 @@ func (r *DNSRecord) getName(ZoneName string, Zone *cloudflare.Zone) (string) {
return built
}
func (r *DNSRecord) handle(ctx *pulumi.Context, ZoneName string, zone *cloudflare.Zone) (*cloudflare.Record, error) {
func (r *DNSRecord) handle(ctx *pulumi.Context, zone *Zone) (err error) {
r.Zone = zone
var recordArgs *cloudflare.RecordArgs
switch r.Kind {
case CAA:
recordArgs = &cloudflare.RecordArgs{
ZoneId: r.getZone(zone),
ZoneId: r.getZone(),
Name: pulumi.String(r.Name),
Type: pulumi.String(strings.ToUpper(string(r.Kind))),
Ttl: pulumi.Int(r.Ttl),
@ -81,7 +89,7 @@ func (r *DNSRecord) handle(ctx *pulumi.Context, ZoneName string, zone *cloudflar
}
default:
recordArgs = &cloudflare.RecordArgs{
ZoneId: r.getZone(zone),
ZoneId: r.getZone(),
Name: pulumi.String(r.Name),
Type: pulumi.String(strings.ToUpper(string(r.Kind))),
Ttl: pulumi.Int(r.Ttl),
@ -89,5 +97,6 @@ func (r *DNSRecord) handle(ctx *pulumi.Context, ZoneName string, zone *cloudflar
Value: pulumi.String(r.Value),
}
}
return cloudflare.NewRecord(ctx, r.getName(ZoneName, zone), recordArgs)
r.CFRecord, err = cloudflare.NewRecord(ctx, r.getName(), recordArgs, pulumi.DependsOn([]pulumi.Resource{r.Zone.CFZone}))
return err
}

View file

@ -0,0 +1,91 @@
package iac
import (
"crypto/rand"
"crypto/rsa"
"fmt"
tls "github.com/pulumi/pulumi-tls/sdk/v4/go/tls"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
"golang.org/x/crypto/ssh"
"time"
)
// ca_key *tls.PrivateKey,
// ca_cert *tls.SelfSignedCert) (key *tls.PrivateKey,
// ca_key, ca_cert, err := iac.GenerateTLSCA(ctx)
// parseprivatekey
// newsignerfromkey
func MakeCertificate() ssh.Certificate {
var newCert ssh.Certificate
// The sign() method fills in Nonce for us
newCert.Nonce = make([]byte, 32)
return newCert
}
func PrivateKeyOpenSSHToRSAPrivateKey(keyPEM string) (key *rsa.PrivateKey, err error) {
key_int, err := ssh.ParseRawPrivateKey([]byte(keyPEM))
key_raw := key_int.(*rsa.PrivateKey)
if err != nil {
return nil, err
}
return key_raw, err
}
func GenerateOpenSSHHost(caKey *tls.PrivateKey, userKey *tls.PrivateKey, keyID string, name string) (certificate pulumi.StringOutput, err error) {
return GenerateOpenSSH(caKey, userKey, keyID, ssh.HostCert, name)
}
func GenerateOpenSSHUser(caKey *tls.PrivateKey, userKey *tls.PrivateKey, keyID string, name string) (certificate pulumi.StringOutput, err error) {
return GenerateOpenSSH(caKey, userKey, keyID, ssh.UserCert, name)
}
func GenerateOpenSSH(caKey *tls.PrivateKey, userKey *tls.PrivateKey, keyID string, certType uint32, name string) (certificate pulumi.StringOutput, err error) {
var caKeyPem *rsa.PrivateKey
var signer ssh.Signer
newCert := caKey.PrivateKeyOpenssh.ApplyT(func(capriv string) (cert pulumi.StringOutput) {
newCertS := userKey.PrivateKeyOpenssh.ApplyT(func(content string) (cert pulumi.String) {
caKeyPem, err = PrivateKeyOpenSSHToRSAPrivateKey(capriv)
if err != nil {
panic(err)
}
signer, err = ssh.NewSignerFromKey(caKeyPem)
if err != nil {
panic(err)
}
newCert := MakeCertificate()
newCert.CertType = certType
key, err := PrivateKeyOpenSSHToRSAPrivateKey(content)
if err != nil {
panic(err)
}
newCert.Key, err = ssh.NewPublicKey(key.Public())
if err != nil {
panic(err)
}
newCert.Serial = 0
newCert.KeyId = keyID
newCert.ValidPrincipals = []string{fmt.Sprintf("%s.inskip.me", name)}
newCert.ValidAfter = 60
threemo, err := time.ParseDuration("730h")
if err != nil {
panic(err)
}
threemosecs := uint64(threemo.Seconds())
newCert.ValidBefore = threemosecs
err = newCert.SignCert(rand.Reader, signer)
return pulumi.String(string(ssh.MarshalAuthorizedKey(&newCert)))
}).(pulumi.StringOutput)
if err != nil {
panic(err)
}
return newCertS
}).(pulumi.StringOutput)
if err != nil {
return pulumi.StringOutput{}, err
}
return newCert, err
}

View file

@ -1,110 +0,0 @@
package iac
import (
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
tailscale "github.com/pulumi/pulumi-tailscale/sdk/go/tailscale"
cloudflare "github.com/pulumi/pulumi-cloudflare/sdk/v4/go/cloudflare"
tls "github.com/pulumi/pulumi-tls/sdk/v4/go/tls"
"strings"
"net"
"fmt"
)
func MakeRecord(ctx *pulumi.Context, zones map[string]*cloudflare.Zone, name string, address string) (record *cloudflare.Record, index string, err error) {
ip := net.ParseIP(address)
kind := A;
if ip.To4() == nil {
kind = AAAA;
}
record_ := DNSRecord{
Name: name,
Kind: kind,
Value: ip.String(),
Ttl: 3600,
}
record, err = record_.handle(ctx, "inskip", zones["inskip"])
index = record_.getName("inskip", zones["inskip"])
if err != nil {
return nil, "", err
}
return record, index, err
}
func HandleTSRecord(ctx *pulumi.Context, zones map[string]*cloudflare.Zone, device tailscale.GetDevicesDevice) (new_records map[string]*cloudflare.Record, err error) {
if device.User != "kat@inskip.me" {
return nil, nil
}
new_records = make(map[string]*cloudflare.Record)
name := strings.Split(device.Name, ".")[0]
for _, address := range device.Addresses {
new_record, index, err := MakeRecord(ctx, zones, name, address)
new_records[index] = new_record
if err != nil {
return nil, err
}
}
return new_records, err
}
func HandleTSRecords(ctx *pulumi.Context,
tailnet *tailscale.GetDevicesResult,
zones map[string]*cloudflare.Zone,
input_records map[string]*cloudflare.Record,
) (records map[string]*cloudflare.Record, err error) {
for _, device := range tailnet.Devices {
new_records, err := HandleTSRecord(ctx, zones, device)
if err != nil {
return nil, err
}
for k,v := range new_records {
input_records[k] = v
}
records = input_records
}
return records, err
}
func HandleTSHostCert(ctx *pulumi.Context,
device tailscale.GetDevicesDevice,
ca_key *tls.PrivateKey,
ca_cert *tls.SelfSignedCert) (key *tls.PrivateKey,
cr *tls.CertRequest,
cert *tls.LocallySignedCert,
err error) {
name := strings.Split(device.Name, ".")[0]
key, cr, cert, err = generateKeyPair(
ctx,
fmt.Sprintf("ts-%s-host", name),
ca_key,
ca_cert,
[]string{fmt.Sprintf("%s.inskip.me", name)},
device.Addresses,
)
if err != nil {
return nil, nil, nil, err
}
return key, cr, cert, err
}
func HandleTSHostCerts(ctx *pulumi.Context,
tailnet *tailscale.GetDevicesResult,
ca_key *tls.PrivateKey,
ca_cert *tls.SelfSignedCert) (keys map[string]*tls.PrivateKey,
crs map[string]*tls.CertRequest,
certs map[string]*tls.LocallySignedCert,
err error) {
keys = make(map[string]*tls.PrivateKey)
crs = make(map[string]*tls.CertRequest)
certs = make(map[string]*tls.LocallySignedCert)
for _, device := range tailnet.Devices {
if device.User != "kat@inskip.me" {
continue
}
name := strings.Split(device.Name, ".")[0]
keys[fmt.Sprintf("ts-%s-host-key", name)], crs[fmt.Sprintf("ts-%s-host-cr", name)], certs[fmt.Sprintf("ts-%s-host-cert", name)], err = HandleTSHostCert(ctx, device, ca_key, ca_cert)
if err != nil {
return nil, nil, nil, err
}
}
return keys, crs, certs, err
}

View file

@ -1,54 +0,0 @@
package iac
import (
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
tls "github.com/pulumi/pulumi-tls/sdk/v4/go/tls"
"fmt"
)
func generateKeyPair(ctx *pulumi.Context,
purpose string,
ca_key *tls.PrivateKey,
ca_cert *tls.SelfSignedCert,
dns_names []string,
ip_addresses []string) (key *tls.PrivateKey,
cr *tls.CertRequest,
cert *tls.LocallySignedCert,
err error) {
key, err = tls.NewPrivateKey(ctx, fmt.Sprintf("%s-key", purpose), &tls.PrivateKeyArgs{
Algorithm: pulumi.String("RSA"),
RsaBits: pulumi.Int(4096),
}, pulumi.DependsOn([]pulumi.Resource{ca_key, ca_cert}))
if err != nil {
return nil, nil, nil, err
}
cr, err = tls.NewCertRequest(ctx, fmt.Sprintf("%s-cr", purpose), &tls.CertRequestArgs{
PrivateKeyPem: key.PrivateKeyPem,
DnsNames: goStringArrayToPulumiStringArray(dns_names),
IpAddresses: goStringArrayToPulumiStringArray(ip_addresses),
Subject: &tls.CertRequestSubjectArgs{
CommonName: pulumi.String("inskip.me"),
Organization: pulumi.String("Kat Inskip"),
},
}, pulumi.DependsOn([]pulumi.Resource{ca_key, ca_cert, key}))
if err != nil {
return nil, nil, nil, err
}
cert, err = tls.NewLocallySignedCert(ctx, fmt.Sprintf("%s-cert", purpose), &tls.LocallySignedCertArgs{
AllowedUses: goStringArrayToPulumiStringArray([]string{"digital_signature",
"digital_signature",
"key_encipherment",
"key_agreement",
"email_protection",
}),
CaPrivateKeyPem: ca_key.PrivateKeyPem,
CaCertPem: ca_cert.CertPem,
CertRequestPem: cr.CertRequestPem,
ValidityPeriodHours: pulumi.Int(1440),
EarlyRenewalHours: pulumi.Int(168),
}, pulumi.DependsOn([]pulumi.Resource{ca_key, ca_cert, key, cr}))
if err != nil {
return nil, nil, nil, err
}
return key, cr, cert, err
}

View file

@ -1,24 +1,63 @@
package iac
import (
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
cloudflare "github.com/pulumi/pulumi-cloudflare/sdk/v4/go/cloudflare"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
"strings"
)
type Zone struct {
Context *pulumi.Context
Alias string
Zone string `yaml:"name"`
Records []DNSRecord `yaml:"records"`
ExtraRecords []DNSRecord `yaml:"records"`
CFZone *cloudflare.Zone
Devices []Device
CertAuth CertificateAuthority
DNSSec *cloudflare.ZoneDnssec
}
func (z *Zone) handle(ctx *pulumi.Context, name string) (zone *cloudflare.Zone, err error) {
zone, err = cloudflare.NewZone(ctx, name, &cloudflare.ZoneArgs{
func (z *Zone) Handle(ctx *pulumi.Context) (err error) {
z.Context = ctx
z.Alias = strings.ReplaceAll(z.Zone, ".", "-")
z.CFZone, err = cloudflare.NewZone(ctx, z.Alias, &cloudflare.ZoneArgs{
AccountId: pulumi.ID("0467b993b65d8fd4a53fe24ed2fbb2a1"),
Zone: pulumi.String(z.Zone),
Plan: pulumi.String("free"),
})
if z.Alias == "inskip-me" {
z.CertAuth = CertificateAuthority{}
err = z.CertAuth.handle(ctx)
if err != nil {
return nil, err
return err
}
return zone, err
err = z.handleTailscale()
if err != nil {
return err
}
}
for _, record := range z.ExtraRecords {
err = record.handle(ctx, z)
}
return err
}
func (z *Zone) dnssec() (err error) {
z.DNSSec, err = cloudflare.NewZoneDnssec(z.Context, z.Alias, &cloudflare.ZoneDnssecArgs{
ZoneId: z.CFZone.ID(),
})
if err != nil {
return err
}
return err
}
func (z *Zone) handleTailscale() (err error) {
tailnet := Tailnet{}
err = tailnet.handle(z.Context, z, z.CertAuth.Key, z.CertAuth.Cert)
if err != nil {
return err
}
z.Devices = tailnet.Devices
return err
}

View file

@ -2,6 +2,7 @@
users.users.kat = {
uid = 1000;
isNormalUser = true;
hashedPassword = "$6$G26zDwcywO6$YzHK1YI6X0d7x/mV6maCx6B7V3M1JdE3VqxxjNc7muxUPkZo0YYwniAB2";
openssh.authorizedKeys = {
inherit (tree.kat.user.data) keys;
};

42
main.go
View file

@ -2,15 +2,13 @@ package main
import (
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
"github.com/pulumi/pulumi-tailscale/sdk/go/tailscale"
"gopkg.in/yaml.v3"
"os"
"kittywitch/iac"
"github.com/pulumi/pulumi-command/sdk/go/command/local"
"os"
)
func main() {
katConfig := iac.KatConfig{}
store := iac.KatConfig{}
configFile, err := os.ReadFile("config.yaml")
@ -18,47 +16,19 @@ func main() {
return
}
if err := yaml.Unmarshal(configFile, &katConfig); err != nil {
if err := yaml.Unmarshal(configFile, &store); err != nil {
return
}
pulumi.Run(func(ctx *pulumi.Context) error {
tailnet, err := tailscale.GetDevices(ctx, &tailscale.GetDevicesArgs{}, nil)
for _, zone := range store.Zones {
err = zone.Handle(ctx)
if err != nil {
return err
}
// zones, dnssec, records
zones, _, records, err := iac.HandleDNS(ctx, katConfig)
if err != nil {
return err
}
records, err = iac.HandleTSRecords(ctx, tailnet, zones, records)
if err != nil {
return err
}
ca_key, ca_cert, err := iac.GenerateTLSCA(ctx)
if err != nil {
return err
}
keys, _, certs, err := iac.HandleTSHostCerts(ctx, tailnet, ca_key, ca_cert)
if err != nil {
return err
}
// files for those certs
files := make(map[string]*local.Command)
files, err = iac.PKITLSFiles(ctx, files, keys, certs)
err = iac.InskipPage(ctx)
if err != nil {
return err
}

View file

@ -1,6 +1,6 @@
{ lib, config, inputs, ... }: let
# TODO: convert to nix-std
inherit (lib.attrsets) mapAttrsToList mapAttrs;
inherit (lib.attrsets) mapAttrsToList mapAttrs filterAttrs;
inherit (lib.lists) optionals;
inherit (lib.options) mkOption;
inherit (lib.types) int attrsOf submodule;
@ -13,7 +13,7 @@
maxJobs = 100;
speedFactor = config.distributed.outputs.${name};
supportedFeatures = [ "nixos-test" "benchmark" "big-parallel" "kvm" ];
} ) (inputs.self.nixosConfigurations // inputs.self.darwinConfigurations);
} ) (filterAttrs (n: _: n != config.networking.hostName) (inputs.self.nixosConfigurations // inputs.self.darwinConfigurations));
daiyousei = {
hostName = "daiyousei.inskip.me";
sshUser = "root";

View file

@ -9,6 +9,17 @@ _: let
distributed.systems.renko.preference = 5;
environment.systemPackages = with pkgs; [
fd # fd, better fine!
ripgrep # rg, better grep!
go # Required for pulumi
pulumi-bin # Infrastructure as code
deadnix # dead-code scanner
alejandra # code formatter
statix # anti-pattern finder
deploy-rs.deploy-rs # deployment system
];
homebrew = {
brewPrefix = "/opt/homebrew/bin";
brews = [