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: config:
cloudflare:apiToken: cloudflare:apiToken:
secure: AAABACPPwldgPSVHPYdh4lQnYR+baaGsnQRwGw/qwNke8wuFnCTdZIqj6yXu0CCGaYBu0+66LTALNNl9mo9HXs/PMLmoOqLo secure: AAABAFcufTX7tZZf2gcK6hML2tgovDEfcPAJcgjfYkV3GMS4Ilwzuco5p+hCpyj3vCm7cqm3tmdwOLlOFxGqKZGRj+ESXAzv
tailscale:apiKey: tailscale:apiKey:
secure: AAABAGc7s7XJ+voSUNcMmRuVwrUdx3kojn0fdEl6qpUy0WmhgHbk6cEz2/kGSEGhuLGwo3mzOGVTI+NVu6/Xz4PmE9FME++VfE8cz5DFjDrMJ4JdX0DR secure: AAABAGc7s7XJ+voSUNcMmRuVwrUdx3kojn0fdEl6qpUy0WmhgHbk6cEz2/kGSEGhuLGwo3mzOGVTI+NVu6/Xz4PmE9FME++VfE8cz5DFjDrMJ4JdX0DR
tailscale:tailnet: inskip.me tailscale:tailnet: inskip.me

View file

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

View file

@ -1,5 +1,16 @@
_: { { pkgs, ... }: {
nix.envVars = { nix.envVars = {
"SSH_AUTH_SOCK" = "/Users/kat/.gnupg/S.gpg-agent.ssh"; "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": { "arcexprs": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1674878824, "lastModified": 1675107003,
"narHash": "sha256-skuyKtydzGFtd2UQB2BZW2bMaUcS+fvHEEz+H4MNQ4g=", "narHash": "sha256-ao5OLwhC7+T3O2ixRrt76gIg9uZR3c17SJL3Wvx0EpQ=",
"owner": "arcnmx", "owner": "arcnmx",
"repo": "nixexprs", "repo": "nixexprs",
"rev": "ab1bd348da95d6f33ad28992b5d8c636d2330cc9", "rev": "5b7d1eb5e578da7ed36b2105f80f82f9ad11244d",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -126,11 +126,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1674771519, "lastModified": 1675181178,
"narHash": "sha256-U0W3S1nX6yEvLh3Vq70EORbmXecAKXfmEfCfaA4A+I8=", "narHash": "sha256-jymSUUjKoArptU7LJ1i4boysXptnpuETiUTenKgs2fM=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "bb4b25b302dbf0f527f190461b080b5262871756", "rev": "69696fe53940562a047bf2ec675cc1dcd1bc09b3",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -173,11 +173,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1674357402, "lastModified": 1674962474,
"narHash": "sha256-oxOCYORXBh0KW4KEwKpzs2LThDdVmEwREV+SsP4CIpg=", "narHash": "sha256-qEXdgW5fnMSdQwP1zQYa0fVtI0f3G1f2qNRjUEherCs=",
"owner": "Mic92", "owner": "Mic92",
"repo": "nix-index-database", "repo": "nix-index-database",
"rev": "e9e7c5c62965e7e656febb5ba578d53f751eb41f", "rev": "a385f6192f5471c4cebeeb0d2e966b5ccf123df5",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -188,11 +188,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1674641431, "lastModified": 1675115703,
"narHash": "sha256-qfo19qVZBP4qn5M5gXc/h1MDgAtPA5VxJm9s8RUAkVk=", "narHash": "sha256-4zetAPSyY0D77x+Ww9QBe8RHn1akvIvHJ/kgg8kGDbk=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "9b97ad7b4330aacda9b2343396eb3df8a853b4fc", "rev": "2caf4ef5005ecc68141ecb4aac271079f7371c44",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -205,11 +205,11 @@
"pypi-deps-db": { "pypi-deps-db": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1674855014, "lastModified": 1675156620,
"narHash": "sha256-SMUq72uWWs+KAlcRB7UXzI1QHNTGiUTpQK2qj1evHXQ=", "narHash": "sha256-lmnsBYJz2Fgm0WFNUgSqskpwa0ffbFOr9YGDZpUXptk=",
"owner": "DavHau", "owner": "DavHau",
"repo": "pypi-deps-db", "repo": "pypi-deps-db",
"rev": "a375715227007ca768d372b2b09bcb76f8f19d78", "rev": "2e236bb32e6e3ef13cd56fc6d9aee8c89059a1ac",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -1,40 +1,45 @@
package iac package iac
import( import (
"github.com/pulumi/pulumi/sdk/v3/go/pulumi" tls "github.com/pulumi/pulumi-tls/sdk/v4/go/tls"
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) { type CertificateAuthority struct {
key, err = tls.NewPrivateKey(ctx, "kat-root-ca-key", &tls.PrivateKeyArgs{ Key *tls.PrivateKey
Algorithm: pulumi.String("RSA"), Cert *tls.SelfSignedCert
RsaBits: pulumi.Int(4096), }
})
func (ca *CertificateAuthority) handle(ctx *pulumi.Context) (err error) {
if err != nil { ca.Key, err = tls.NewPrivateKey(ctx, "ca-root", &tls.PrivateKeyArgs{
return nil, nil, err Algorithm: pulumi.String("RSA"),
} RsaBits: pulumi.Int(4096),
})
cert, err = tls.NewSelfSignedCert(ctx, "kat-root-ca-pem-cert", &tls.SelfSignedCertArgs{
PrivateKeyPem: key.PrivateKeyPem, if err != nil {
AllowedUses: goStringArrayToPulumiStringArray([]string{"digital_signature", return err
"cert_signing", }
"crl_signing"}),
IsCaCertificate: pulumi.Bool(true), ca.Cert, err = tls.NewSelfSignedCert(ctx, "ca-root", &tls.SelfSignedCertArgs{
ValidityPeriodHours: pulumi.Int(2562047), PrivateKeyPem: ca.Key.PrivateKeyPem,
Subject: &tls.SelfSignedCertSubjectArgs{ AllowedUses: goStringArrayToPulumiStringArray([]string{"digital_signature",
CommonName: pulumi.String("inskip.me"), "cert_signing",
Organization: pulumi.String("Kat Inskip"), "crl_signing"}),
}, IsCaCertificate: pulumi.Bool(true),
}) ValidityPeriodHours: pulumi.Int(2562047),
Subject: &tls.SelfSignedCertSubjectArgs{
if err != nil { CommonName: pulumi.String("inskip.me"),
return nil, nil, err Organization: pulumi.String("Kat Inskip"),
} },
})
ctx.Export("tls_ca_pem_key", key.PrivateKeyPem)
ctx.Export("tls_ca_os_key", key.PrivateKeyOpenssh) if err != nil {
ctx.Export("tls_ca_cert", cert.CertPem) return err
}
return key, cert, err
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 err
} }

View file

@ -1,5 +1,5 @@
package iac package iac
type KatConfig struct { type KatConfig struct {
Zones map[string]Zone `yaml:"zones"` Zones map[string]Zone `yaml:"zones"`
} }

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 package iac
import ( 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/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) { func CreatePulumiFile(ctx *pulumi.Context, name string, fqdn string, value pulumi.StringOutput, resources []pulumi.Resource) (*remote.Command, error) {
repo_root := os.Getenv("REPO_ROOT") environment := goMapToPulumiMap(map[string]string{
data_root := path.Join(repo_root, "./data") "PULUMI_SKIP_UPDATE_CHECK": "true",
ctx.Export(name, value) })
return local.NewCommand(ctx, name, &local.CommandArgs{ return remote.NewCommand(ctx, name, &remote.CommandArgs{
Create: pulumi.String(fmt.Sprintf("pulumi stack output %s --non-interactive --show-secrets > %s", name, name)), Connection: &remote.ConnectionArgs{
Update: pulumi.String(fmt.Sprintf("pulumi stack output %s --non-interactive --show-secrets > %s", name, name)), Host: pulumi.String(fqdn),
Delete: pulumi.String(fmt.Sprintf("rm %s", name)), Port: pulumi.Float64Ptr(22),
Dir: pulumi.String(data_root), User: pulumi.String("deploy"),
Environment: goMapToPulumiMap(map[string]string{ AgentSocketPath: pulumi.String("/Users/kat/.gnupg/S.gpg-agent.ssh"),
"PULUMI_SKIP_UPDATE_CHECK": "true", },
}), 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),
}, pulumi.DependsOn([]pulumi.Resource{resource})) Delete: pulumi.Sprintf("cd /var/lib/secrets && rm %s", name),
} Environment: environment,
}, pulumi.DependsOn(resources))
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
} }

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,93 +1,102 @@
package iac package iac
import ( import (
"github.com/pulumi/pulumi/sdk/v3/go/pulumi" "crypto/md5"
cloudflare "github.com/pulumi/pulumi-cloudflare/sdk/v4/go/cloudflare" "encoding/hex"
"github.com/creasty/defaults" "fmt"
"fmt" "github.com/creasty/defaults"
"strings" "github.com/pulumi/pulumi-cloudflare/sdk/v4/go/cloudflare"
"crypto/md5" "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
"encoding/hex" "strings"
) )
type DNSRecordType string type DNSRecordType string
const ( const (
A DNSRecordType = "a" A DNSRecordType = "a"
AAAA = "aaaa" AAAA = "aaaa"
MX = "mx" MX = "mx"
TXT = "txt" TXT = "txt"
CAA = "caa" CAA = "caa"
CNAME = "cname"
) )
type DNSRecord struct { type DNSRecord struct {
Name string `default:"@" yaml:"name"` CFRecord *cloudflare.Record
Kind DNSRecordType `yaml:"kind"` Zone *Zone
Value string `yaml:"value,omitempty"` Name string `default:"@" yaml:"name"`
Priority int `yaml:"priority,omitempty"` Kind DNSRecordType `yaml:"kind"`
Flags string `yaml:"flags,omitempty"` Value string `yaml:"value,omitempty"`
Tag string `yaml:"tag,omitempty"` Priority int `yaml:"priority,omitempty"`
Ttl int `default:"3600" yaml:"ttl,omitempty"` Flags string `yaml:"flags,omitempty"`
Tag string `yaml:"tag,omitempty"`
Ttl int `default:"3600" yaml:"ttl,omitempty"`
} }
func (r *DNSRecord) UnmarshalYAML(unmarshal func(interface{}) error) error { func (r *DNSRecord) UnmarshalYAML(unmarshal func(interface{}) error) (err error) {
defaults.Set(r) err = defaults.Set(r)
type plain DNSRecord if err != nil {
if err := unmarshal((*plain)(r)); err != nil { return err
return err }
}
return nil type plain DNSRecord
if err := unmarshal((*plain)(r)); err != nil {
return err
}
return err
} }
func (r *DNSRecord) getZone(Zone *cloudflare.Zone) (pulumi.StringOutput) { func (r *DNSRecord) getZone() pulumi.StringOutput {
return Zone.ID().ToStringOutput() return r.Zone.CFZone.ID().ToStringOutput()
} }
func (r *DNSRecord) getName(ZoneName string, Zone *cloudflare.Zone) (string) { func (r *DNSRecord) getName() string {
base := fmt.Sprintf("%s-%s-%s", ZoneName, r.Kind, r.Name) base := fmt.Sprintf("%s-%s-%s", r.Zone.Alias, r.Kind, r.Name)
hash := md5.Sum([]byte(r.Value)) hash := md5.Sum([]byte(r.Value))
hashString := hex.EncodeToString(hash[:])[:5] hashString := hex.EncodeToString(hash[:])[:5]
suffix := "" suffix := ""
switch r.Kind { switch r.Kind {
case MX: case MX:
suffix = fmt.Sprintf("-%d-%s", r.Priority, hashString) suffix = fmt.Sprintf("-%d-%s", r.Priority, hashString)
case CAA: case CAA:
suffix = fmt.Sprintf("%s-%s", r.Flags, r.Tag) suffix = fmt.Sprintf("%s-%s", r.Flags, r.Tag)
case A, AAAA, TXT: case A, AAAA, TXT, CNAME:
suffix = fmt.Sprintf("-%s", hashString) suffix = fmt.Sprintf("-%s", hashString)
} }
built := base + suffix built := base + suffix
return built 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) {
var recordArgs *cloudflare.RecordArgs r.Zone = zone
switch r.Kind { var recordArgs *cloudflare.RecordArgs
case CAA: switch r.Kind {
recordArgs = &cloudflare.RecordArgs{ case CAA:
ZoneId: r.getZone(zone), recordArgs = &cloudflare.RecordArgs{
Name: pulumi.String(r.Name), ZoneId: r.getZone(),
Type: pulumi.String(strings.ToUpper(string(r.Kind))), Name: pulumi.String(r.Name),
Ttl: pulumi.Int(r.Ttl), Type: pulumi.String(strings.ToUpper(string(r.Kind))),
Data: &cloudflare.RecordDataArgs{ Ttl: pulumi.Int(r.Ttl),
Flags: pulumi.String(r.Flags), Data: &cloudflare.RecordDataArgs{
Tag: pulumi.String(r.Tag), Flags: pulumi.String(r.Flags),
Value: pulumi.String(r.Value), Tag: pulumi.String(r.Tag),
}, Value: pulumi.String(r.Value),
} },
default: }
recordArgs = &cloudflare.RecordArgs{ default:
ZoneId: r.getZone(zone), recordArgs = &cloudflare.RecordArgs{
Name: pulumi.String(r.Name), ZoneId: r.getZone(),
Type: pulumi.String(strings.ToUpper(string(r.Kind))), Name: pulumi.String(r.Name),
Ttl: pulumi.Int(r.Ttl), Type: pulumi.String(strings.ToUpper(string(r.Kind))),
Priority: pulumi.Int(r.Priority), Ttl: pulumi.Int(r.Ttl),
Value: pulumi.String(r.Value), Priority: pulumi.Int(r.Priority),
} 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 package iac
import ( import (
"github.com/pulumi/pulumi/sdk/v3/go/pulumi" cloudflare "github.com/pulumi/pulumi-cloudflare/sdk/v4/go/cloudflare"
cloudflare "github.com/pulumi/pulumi-cloudflare/sdk/v4/go/cloudflare" "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
"strings"
) )
type Zone struct { type Zone struct {
Zone string `yaml:"name"` Context *pulumi.Context
Records []DNSRecord `yaml:"records"` Alias string
Zone string `yaml:"name"`
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) { func (z *Zone) Handle(ctx *pulumi.Context) (err error) {
zone, err = cloudflare.NewZone(ctx, name, &cloudflare.ZoneArgs{ z.Context = ctx
AccountId: pulumi.ID("0467b993b65d8fd4a53fe24ed2fbb2a1"), z.Alias = strings.ReplaceAll(z.Zone, ".", "-")
Zone: pulumi.String(z.Zone), z.CFZone, err = cloudflare.NewZone(ctx, z.Alias, &cloudflare.ZoneArgs{
Plan: pulumi.String("free"), AccountId: pulumi.ID("0467b993b65d8fd4a53fe24ed2fbb2a1"),
}) Zone: pulumi.String(z.Zone),
if err != nil { Plan: pulumi.String("free"),
return nil, err })
} if z.Alias == "inskip-me" {
return zone, err z.CertAuth = CertificateAuthority{}
err = z.CertAuth.handle(ctx)
if err != nil {
return 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 = { users.users.kat = {
uid = 1000; uid = 1000;
isNormalUser = true; isNormalUser = true;
hashedPassword = "$6$G26zDwcywO6$YzHK1YI6X0d7x/mV6maCx6B7V3M1JdE3VqxxjNc7muxUPkZo0YYwniAB2";
openssh.authorizedKeys = { openssh.authorizedKeys = {
inherit (tree.kat.user.data) keys; inherit (tree.kat.user.data) keys;
}; };

80
main.go
View file

@ -1,68 +1,38 @@
package main package main
import ( import (
"github.com/pulumi/pulumi/sdk/v3/go/pulumi" "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
"github.com/pulumi/pulumi-tailscale/sdk/go/tailscale" "gopkg.in/yaml.v3"
"gopkg.in/yaml.v3" "kittywitch/iac"
"os" "os"
"kittywitch/iac"
"github.com/pulumi/pulumi-command/sdk/go/command/local"
) )
func main() { func main() {
katConfig := iac.KatConfig{} store := iac.KatConfig{}
configFile, err := os.ReadFile("config.yaml") configFile, err := os.ReadFile("config.yaml")
if err != nil { if err != nil {
return return
} }
if err := yaml.Unmarshal(configFile, &katConfig); err != nil { if err := yaml.Unmarshal(configFile, &store); err != nil {
return return
} }
pulumi.Run(func(ctx *pulumi.Context) error { pulumi.Run(func(ctx *pulumi.Context) error {
tailnet, err := tailscale.GetDevices(ctx, &tailscale.GetDevicesArgs{}, nil) for _, zone := range store.Zones {
if err != nil { err = zone.Handle(ctx)
return err if err != nil {
} return err
}
}
// zones, dnssec, records err = iac.InskipPage(ctx)
zones, _, records, err := iac.HandleDNS(ctx, katConfig) if err != nil {
return err
}
if err != nil { return err
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)
if err != nil {
return err
}
return err
})
} }

View file

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

View file

@ -9,6 +9,17 @@ _: let
distributed.systems.renko.preference = 5; 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 = { homebrew = {
brewPrefix = "/opt/homebrew/bin"; brewPrefix = "/opt/homebrew/bin";
brews = [ brews = [